mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
style: format extracted place and route modules
This commit is contained in:
1 parent
2793d7a8df
commit
7e6d89d8f5
9 files changed
+552
-309
No files matched your search
@@ -8,10 +8,11 @@
|
||||
* @returns {number} Distance in kilometers.
|
||||
*/
|
||||
export function haversineKm(lat1, lon1, lat2, lon2) {
|
||||
const toRad = (value) => value * Math.PI / 180;
|
||||
const toRad = (value) => (value * Math.PI) / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLon = toRad(lon2 - lon1);
|
||||
const a = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||
const a =
|
||||
Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||
return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
@@ -13,7 +13,10 @@ export function makeRateLimiter({ windowMs, max, globalMax }) {
|
||||
globalTimes = globalTimes.filter((t) => now - t < windowMs);
|
||||
if (globalMax && globalTimes.length >= globalMax) return false; // global backstop
|
||||
const recent = (hits.get(key) || []).filter((t) => now - t < windowMs);
|
||||
if (recent.length >= max) { hits.set(key, recent); return false; }
|
||||
if (recent.length >= max) {
|
||||
hits.set(key, recent);
|
||||
return false;
|
||||
}
|
||||
recent.push(now);
|
||||
hits.set(key, recent);
|
||||
globalTimes.push(now);
|
||||
@@ -46,7 +49,11 @@ export function makeRateLimiter({ windowMs, max, globalMax }) {
|
||||
export function makeOptInRateLimiter(envValue) {
|
||||
const max = Number(envValue);
|
||||
if (!Number.isFinite(max) || max <= 0) return null; // unset/0/garbage -> unlimited
|
||||
return makeRateLimiter({ windowMs: 60_000, max: Math.floor(max), globalMax: Math.floor(max) * 20 });
|
||||
return makeRateLimiter({
|
||||
windowMs: 60_000,
|
||||
max: Math.floor(max),
|
||||
globalMax: Math.floor(max) * 20,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
/** Place-search and route middleware for Node development servers. */
|
||||
export { googlePlacesContextProxy } from './places/google.js';
|
||||
export { googleServerApiKey, keylessGooglePlacesResponse } from './places/google-key.js';
|
||||
export {
|
||||
googleServerApiKey,
|
||||
keylessGooglePlacesResponse,
|
||||
} from './places/google-key.js';
|
||||
export { installRouteMiddleware } from './places/routes.js';
|
||||
export { makeRateLimiter, makeOptInRateLimiter, clientKey } from './common/rate-limit.js';
|
||||
export {
|
||||
makeRateLimiter,
|
||||
makeOptInRateLimiter,
|
||||
clientKey,
|
||||
} from './common/rate-limit.js';
|
||||
export { haversineKm } from './common/geo.js';
|
||||
@@ -1,6 +1,12 @@
|
||||
import { googleServerApiKey, keylessGooglePlacesResponse } from './google-key.js';
|
||||
import {
|
||||
googleServerApiKey,
|
||||
keylessGooglePlacesResponse,
|
||||
} from './google-key.js';
|
||||
import { makeOptInRateLimiter, clientKey } from '../common/rate-limit.js';
|
||||
import { projectNearbyPlaces, projectTextSearchPlaces } from '../../../src/data/placeProviderPayloads.js';
|
||||
import {
|
||||
projectNearbyPlaces,
|
||||
projectTextSearchPlaces,
|
||||
} from '../../../src/data/placeProviderPayloads.js';
|
||||
|
||||
// Construct lazily after the standalone environment has loaded.
|
||||
// undefined = not built yet; null = unlimited; fn = active limiter
|
||||
@@ -8,12 +14,17 @@ let _googleRateLimiter;
|
||||
|
||||
/** Google cost endpoint (nearby-places). Null = unlimited (default). */
|
||||
function googleRateLimiter() {
|
||||
if (_googleRateLimiter === undefined) _googleRateLimiter = makeOptInRateLimiter(process.env.GEV_RATELIMIT_GOOGLE_PER_MIN);
|
||||
if (_googleRateLimiter === undefined)
|
||||
_googleRateLimiter = makeOptInRateLimiter(
|
||||
process.env.GEV_RATELIMIT_GOOGLE_PER_MIN,
|
||||
);
|
||||
return _googleRateLimiter;
|
||||
}
|
||||
|
||||
/** Nearby place labels and view-biased text search, with request-time key resolution. */
|
||||
export function googlePlacesContextProxy({ resolveApiKey = googleServerApiKey } = {}) {
|
||||
export function googlePlacesContextProxy({
|
||||
resolveApiKey = googleServerApiKey,
|
||||
} = {}) {
|
||||
function install(middlewares) {
|
||||
middlewares.use('/api/google/nearby-places', async (req, res) => {
|
||||
if (req.method !== 'GET') {
|
||||
@@ -51,56 +62,76 @@ export function googlePlacesContextProxy({ resolveApiKey = googleServerApiKey }
|
||||
const requestUrl = new URL(req.url || '', 'http://localhost');
|
||||
const latitude = Number(requestUrl.searchParams.get('lat'));
|
||||
const longitude = Number(requestUrl.searchParams.get('lon'));
|
||||
const radiusM = Math.max(25, Math.min(5000, Number(requestUrl.searchParams.get('radiusM')) || 250));
|
||||
const radiusM = Math.max(
|
||||
25,
|
||||
Math.min(5000, Number(requestUrl.searchParams.get('radiusM')) || 250),
|
||||
);
|
||||
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'Valid lat and lon are required', places: [] }));
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: 'Valid lat and lon are required',
|
||||
places: [],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('https://places.googleapis.com/v1/places:searchNearby', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': apiKey,
|
||||
'X-Goog-FieldMask': [
|
||||
'places.id',
|
||||
'places.displayName',
|
||||
'places.formattedAddress',
|
||||
'places.shortFormattedAddress',
|
||||
'places.location',
|
||||
'places.primaryType',
|
||||
'places.primaryTypeDisplayName',
|
||||
'places.types',
|
||||
].join(','),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
maxResultCount: 20,
|
||||
rankPreference: 'DISTANCE',
|
||||
locationRestriction: {
|
||||
circle: {
|
||||
center: { latitude, longitude },
|
||||
radius: radiusM,
|
||||
},
|
||||
const response = await fetch(
|
||||
'https://places.googleapis.com/v1/places:searchNearby',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': apiKey,
|
||||
'X-Goog-FieldMask': [
|
||||
'places.id',
|
||||
'places.displayName',
|
||||
'places.formattedAddress',
|
||||
'places.shortFormattedAddress',
|
||||
'places.location',
|
||||
'places.primaryType',
|
||||
'places.primaryTypeDisplayName',
|
||||
'places.types',
|
||||
].join(','),
|
||||
},
|
||||
}),
|
||||
});
|
||||
body: JSON.stringify({
|
||||
maxResultCount: 20,
|
||||
rankPreference: 'DISTANCE',
|
||||
locationRestriction: {
|
||||
circle: {
|
||||
center: { latitude, longitude },
|
||||
radius: radiusM,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
const places = projectNearbyPlaces(data, latitude, longitude);
|
||||
|
||||
res.statusCode = response.ok ? 200 : response.status;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'private, max-age=300');
|
||||
res.end(JSON.stringify({
|
||||
places,
|
||||
error: response.ok ? null : data.error?.message || 'Google Places request failed',
|
||||
}));
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
places,
|
||||
error: response.ok
|
||||
? null
|
||||
: data.error?.message || 'Google Places request failed',
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ error: error?.message || 'Google Places request failed', places: [] }));
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: error?.message || 'Google Places request failed',
|
||||
places: [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,55 +176,76 @@ export function googlePlacesContextProxy({ resolveApiKey = googleServerApiKey }
|
||||
const textQuery = String(requestUrl.searchParams.get('q') || '').trim();
|
||||
const latitude = Number(requestUrl.searchParams.get('lat'));
|
||||
const longitude = Number(requestUrl.searchParams.get('lon'));
|
||||
const radiusM = Math.max(50, Math.min(50000, Number(requestUrl.searchParams.get('radiusM')) || 4000));
|
||||
if (!textQuery || !Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
||||
const radiusM = Math.max(
|
||||
50,
|
||||
Math.min(50000, Number(requestUrl.searchParams.get('radiusM')) || 4000),
|
||||
);
|
||||
if (
|
||||
!textQuery ||
|
||||
!Number.isFinite(latitude) ||
|
||||
!Number.isFinite(longitude)
|
||||
) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'q, lat and lon are required', places: [] }));
|
||||
res.end(
|
||||
JSON.stringify({ error: 'q, lat and lon are required', places: [] }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('https://places.googleapis.com/v1/places:searchText', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': apiKey,
|
||||
'X-Goog-FieldMask': [
|
||||
'places.id',
|
||||
'places.displayName',
|
||||
'places.formattedAddress',
|
||||
'places.location',
|
||||
'places.viewport',
|
||||
'places.primaryType',
|
||||
'places.types',
|
||||
].join(','),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
textQuery,
|
||||
locationBias: {
|
||||
circle: {
|
||||
center: { latitude, longitude },
|
||||
radius: radiusM,
|
||||
},
|
||||
const response = await fetch(
|
||||
'https://places.googleapis.com/v1/places:searchText',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-Api-Key': apiKey,
|
||||
'X-Goog-FieldMask': [
|
||||
'places.id',
|
||||
'places.displayName',
|
||||
'places.formattedAddress',
|
||||
'places.location',
|
||||
'places.viewport',
|
||||
'places.primaryType',
|
||||
'places.types',
|
||||
].join(','),
|
||||
},
|
||||
maxResultCount: 5,
|
||||
}),
|
||||
});
|
||||
body: JSON.stringify({
|
||||
textQuery,
|
||||
locationBias: {
|
||||
circle: {
|
||||
center: { latitude, longitude },
|
||||
radius: radiusM,
|
||||
},
|
||||
},
|
||||
maxResultCount: 5,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
const places = projectTextSearchPlaces(data, latitude, longitude);
|
||||
|
||||
res.statusCode = response.ok ? 200 : response.status;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'private, max-age=300');
|
||||
res.end(JSON.stringify({
|
||||
places,
|
||||
error: response.ok ? null : data.error?.message || 'Google Places request failed',
|
||||
}));
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
places,
|
||||
error: response.ok
|
||||
? null
|
||||
: data.error?.message || 'Google Places request failed',
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
res.statusCode = 502;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.end(JSON.stringify({ error: error?.message || 'Google Places request failed', places: [] }));
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: error?.message || 'Google Places request failed',
|
||||
places: [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { makeRateLimiter, clientKey } from '../common/rate-limit.js';
|
||||
import { haversineKm } from '../common/geo.js';
|
||||
import { readResponseTextCapped } from '../common/http.js';
|
||||
import { normalizeRouteProfile, projectRouteResult } from '../../../src/data/placeProviderPayloads.js';
|
||||
import {
|
||||
normalizeRouteProfile,
|
||||
projectRouteResult,
|
||||
} from '../../../src/data/placeProviderPayloads.js';
|
||||
|
||||
/** OSM routing (FOSSGIS OSRM) cache: profile|coords -> { payload, cachedAt }. */
|
||||
const ROUTE_CACHE_MS = 600000;
|
||||
@@ -16,89 +19,115 @@ const ROUTE_MAX_LEG_KM = 600;
|
||||
|
||||
const ROUTE_MAX_TOTAL_KM = 2500;
|
||||
|
||||
const _routeRateLimiter = makeRateLimiter({ windowMs: 60_000, max: 60, globalMax: 200 });
|
||||
const _routeRateLimiter = makeRateLimiter({
|
||||
windowMs: 60_000,
|
||||
max: 60,
|
||||
globalMax: 200,
|
||||
});
|
||||
|
||||
export function installRouteMiddleware(middlewares) {
|
||||
// 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) => {
|
||||
const fail = (msg) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: msg }));
|
||||
};
|
||||
try {
|
||||
if (!_routeRateLimiter(clientKey(req))) {
|
||||
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '5' });
|
||||
res.end(JSON.stringify({ ok: false, error: 'rate limited' }));
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const raw = (url.searchParams.get('profile') || 'foot').toLowerCase();
|
||||
const profile = normalizeRouteProfile(raw);
|
||||
if (!profile) return fail('invalid profile');
|
||||
const osrmProfile = profile === 'car' ? 'driving' : profile;
|
||||
const pairs = (url.searchParams.get('coords') || '').split(';').map((s) => s.trim()).filter(Boolean);
|
||||
if (pairs.length < 2 || pairs.length > 12) return fail('need 2-12 coordinates');
|
||||
const clean = [];
|
||||
const pts = [];
|
||||
for (const pr of pairs) {
|
||||
const parts = pr.split(',');
|
||||
if (parts.length !== 2) return fail('invalid coordinate');
|
||||
const lon = Number(parts[0]);
|
||||
const lat = Number(parts[1]);
|
||||
if (!Number.isFinite(lon) || !Number.isFinite(lat) || Math.abs(lat) > 90 || Math.abs(lon) > 180) {
|
||||
return fail('invalid coordinate');
|
||||
}
|
||||
clean.push(`${lon},${lat}`);
|
||||
pts.push([lon, lat]);
|
||||
}
|
||||
// Reject obviously-abusive spans — a real walking/driving route is local,
|
||||
// so a cross-continent request is either a bug or an attempt to drive
|
||||
// heavy upstream OSRM work.
|
||||
let totalKm = 0;
|
||||
for (let i = 1; i < pts.length; i += 1) {
|
||||
// pts are [lon, lat]; existing haversineKm takes (lat1, lon1, lat2, lon2).
|
||||
const legKm = haversineKm(pts[i - 1][1], pts[i - 1][0], pts[i][1], pts[i][0]);
|
||||
if (legKm > ROUTE_MAX_LEG_KM) return fail('route leg too long');
|
||||
totalKm += legKm;
|
||||
}
|
||||
if (totalKm > ROUTE_MAX_TOTAL_KM) return fail('route too long');
|
||||
const coords = clean.join(';');
|
||||
const cacheKey = `${profile}|${coords}`;
|
||||
const now = Date.now();
|
||||
const cached = _routeCache.get(cacheKey);
|
||||
if (cached && now - cached.cachedAt <= ROUTE_CACHE_MS) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
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 controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 12000);
|
||||
let osrm;
|
||||
try {
|
||||
const upstreamRes = await fetch(upstream, {
|
||||
signal: controller.signal,
|
||||
headers: { 'User-Agent': 'gods-eye-view/dev (local)' },
|
||||
});
|
||||
if (!upstreamRes.ok) return fail('no route found');
|
||||
const ctype = upstreamRes.headers.get('content-type') || '';
|
||||
if (!ctype.includes('json')) return fail('no route found');
|
||||
const text = await readResponseTextCapped(upstreamRes, ROUTE_MAX_RESPONSE_BYTES);
|
||||
osrm = JSON.parse(text);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
const route = osrm?.routes?.[0];
|
||||
if (osrm?.code !== 'Ok' || !route?.geometry?.coordinates?.length) return fail('no route found');
|
||||
const payload = projectRouteResult(route, profile);
|
||||
_routeCache.set(cacheKey, { payload, cachedAt: now });
|
||||
if (_routeCache.size > 200) _routeCache.delete(_routeCache.keys().next().value);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(payload));
|
||||
} catch (e) {
|
||||
console.error('[Route Proxy]', e?.message || e);
|
||||
fail('route proxy error');
|
||||
// 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) => {
|
||||
const fail = (msg) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: msg }));
|
||||
};
|
||||
try {
|
||||
if (!_routeRateLimiter(clientKey(req))) {
|
||||
res.writeHead(429, {
|
||||
'Content-Type': 'application/json',
|
||||
'Retry-After': '5',
|
||||
});
|
||||
res.end(JSON.stringify({ ok: false, error: 'rate limited' }));
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const raw = (url.searchParams.get('profile') || 'foot').toLowerCase();
|
||||
const profile = normalizeRouteProfile(raw);
|
||||
if (!profile) return fail('invalid profile');
|
||||
const osrmProfile = profile === 'car' ? 'driving' : profile;
|
||||
const pairs = (url.searchParams.get('coords') || '')
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (pairs.length < 2 || pairs.length > 12)
|
||||
return fail('need 2-12 coordinates');
|
||||
const clean = [];
|
||||
const pts = [];
|
||||
for (const pr of pairs) {
|
||||
const parts = pr.split(',');
|
||||
if (parts.length !== 2) return fail('invalid coordinate');
|
||||
const lon = Number(parts[0]);
|
||||
const lat = Number(parts[1]);
|
||||
if (
|
||||
!Number.isFinite(lon) ||
|
||||
!Number.isFinite(lat) ||
|
||||
Math.abs(lat) > 90 ||
|
||||
Math.abs(lon) > 180
|
||||
) {
|
||||
return fail('invalid coordinate');
|
||||
}
|
||||
});
|
||||
clean.push(`${lon},${lat}`);
|
||||
pts.push([lon, lat]);
|
||||
}
|
||||
// Reject obviously-abusive spans — a real walking/driving route is local,
|
||||
// so a cross-continent request is either a bug or an attempt to drive
|
||||
// heavy upstream OSRM work.
|
||||
let totalKm = 0;
|
||||
for (let i = 1; i < pts.length; i += 1) {
|
||||
// pts are [lon, lat]; existing haversineKm takes (lat1, lon1, lat2, lon2).
|
||||
const legKm = haversineKm(
|
||||
pts[i - 1][1],
|
||||
pts[i - 1][0],
|
||||
pts[i][1],
|
||||
pts[i][0],
|
||||
);
|
||||
if (legKm > ROUTE_MAX_LEG_KM) return fail('route leg too long');
|
||||
totalKm += legKm;
|
||||
}
|
||||
if (totalKm > ROUTE_MAX_TOTAL_KM) return fail('route too long');
|
||||
const coords = clean.join(';');
|
||||
const cacheKey = `${profile}|${coords}`;
|
||||
const now = Date.now();
|
||||
const cached = _routeCache.get(cacheKey);
|
||||
if (cached && now - cached.cachedAt <= ROUTE_CACHE_MS) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
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 controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 12000);
|
||||
let osrm;
|
||||
try {
|
||||
const upstreamRes = await fetch(upstream, {
|
||||
signal: controller.signal,
|
||||
headers: { 'User-Agent': 'gods-eye-view/dev (local)' },
|
||||
});
|
||||
if (!upstreamRes.ok) return fail('no route found');
|
||||
const ctype = upstreamRes.headers.get('content-type') || '';
|
||||
if (!ctype.includes('json')) return fail('no route found');
|
||||
const text = await readResponseTextCapped(
|
||||
upstreamRes,
|
||||
ROUTE_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
osrm = JSON.parse(text);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
const route = osrm?.routes?.[0];
|
||||
if (osrm?.code !== 'Ok' || !route?.geometry?.coordinates?.length)
|
||||
return fail('no route found');
|
||||
const payload = projectRouteResult(route, profile);
|
||||
_routeCache.set(cacheKey, { payload, cachedAt: now });
|
||||
if (_routeCache.size > 200)
|
||||
_routeCache.delete(_routeCache.keys().next().value);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(payload));
|
||||
} catch (e) {
|
||||
console.error('[Route Proxy]', e?.message || e);
|
||||
fail('route proxy error');
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user