feat(03-02): Fix type + three source adapters (telegram/webview/chat-pin) — GEO-01 wrappers

- types.ts: the shared Fix shape + LocationState machine (type-only, D-04/D-05/D-07)
- telegram.ts: version-gated LocationManager init/getLocation, hard 8s timeout (Pitfall 3),
  null-safe telegramLocationSupported (plain browser/Desktop = false, not TypeError),
  looksStale + haversine + the Android nudge (issue #56 navbar read DISCARDED, then retry)
- webview.ts: one-shot geolocation read, maximumAge: 0 (freshness axis), 8s timeout
- chat-pin.ts: bounded 60s poll of GET /api/location (Bearer token) + POST /api/location-fallback
  throw-on-fail so the ASK VIA CHAT handler can revalidate (D-03)
- telegram/webapp.ts created early (Rule 3: task 1 imports TelegramWebAppLike from task 3's
  file; declared the narrow hand-rolled interface now, verbatim per plan)
This commit is contained in:
araharah-executor
2026-10-22 23:37:58 +08:00
parent 3fd0694ee6
commit 9552e7e4ab
5 changed files with 307 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
/*
* webview.ts — source 2: navigator.geolocation inside the WebView, and the
* Android freshness nudge's reading hand (D-04, D-05).
* -------------------------------------------------------------------
* maximumAge: 0 is load-bearing — D-05 requires the freshest fix, and a
* cached position would silently defeat the freshness axis (the whole
* point of the nudge is to force a NEW read, not to re-read a cache).
* Success AND timeout both resolve; the error callback resolves null so
* the chain advances instead of rejecting. Timeout present for the same
* reason as the telegram wrapper: wedging ACQUIRING is never acceptable.
*/
import type { Fix } from "./types.ts";
export function webviewLocationSupported(): boolean {
return typeof navigator !== "undefined" && typeof navigator.geolocation?.getCurrentPosition === "function";
}
export function readWebviewFix(options: { timeoutMs?: number; nowMs?: () => number } = {}): Promise<Fix | null> {
const timeoutMs = options.timeoutMs ?? 8000;
if (!webviewLocationSupported()) return Promise.resolve(null); // Desktop Telegram, plain browsers without permission
return new Promise((resolve) => {
let settled = false;
const done = (fix: Fix | null): void => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(fix);
};
const timer = setTimeout(() => done(null), timeoutMs);
navigator.geolocation.getCurrentPosition(
(pos) =>
done({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
accuracyM: pos.coords.accuracy ?? null,
headingDeg: pos.coords.heading ?? null,
ts: (options.nowMs ?? Date.now)(),
source: "webview",
}),
() => done(null), // denial / unavailability = null; the chain's normal signal (no error object escapes)
{ enableHighAccuracy: true, timeout: timeoutMs, maximumAge: 0 },
);
});
}