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:
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* chat-pin.ts — source 3: the chat-keyboard pin relay (GEO-02).
|
||||
* -----------------------------------------------------------
|
||||
* Telegram has no push channel from chat → WebView, so the app POLLS
|
||||
* GET /api/location with its memory-held Bearer token until the user
|
||||
* taps the request_location keyboard in chat and the bot relays the
|
||||
* pin into TTL KV.
|
||||
*
|
||||
* The poll is BOUNDED (60s): AWAITING PIN can never persist forever —
|
||||
* the bounded poll is what returns the UI to the denied card instead
|
||||
* of wedging (RESEARCH §Pattern 3). requestChatFallback (the ASK VIA
|
||||
* CHAT trigger) POSTs to /api/location-fallback — implemented in plan
|
||||
* 03-01 task 3 Step D as handleLocationFallback. The pathname is a
|
||||
* CONTRACT with 03-01, not a guess; if 03-01 ever renames the route,
|
||||
* both plans change together. The route is gated identically to the
|
||||
* poll: a 401 means the session expired and the caller must
|
||||
* revalidate (D-03) via createSession, not retry blindly — so this
|
||||
* function THROWS rather than silently leaving the user on a card
|
||||
* whose button appears to do nothing.
|
||||
*/
|
||||
import type { Fix } from "./types.ts";
|
||||
|
||||
export const CHAT_PIN_POLL_MS = 2000;
|
||||
export const CHAT_PIN_TIMEOUT_MS = 60000;
|
||||
|
||||
export async function fetchChatPin(token: string): Promise<Fix | null> {
|
||||
const res = await fetch("/api/location", {
|
||||
method: "GET",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) return null; // 401 (expired session) and 405 both read as "no pin yet"; caller revalidates on the denied path
|
||||
const body: unknown = await res.json();
|
||||
const pin = (body as { pin?: { latitude?: number; longitude?: number; horizontal_accuracy?: number | null } }).pin;
|
||||
if (!pin || typeof pin.latitude !== "number" || typeof pin.longitude !== "number") return null;
|
||||
return {
|
||||
lat: pin.latitude,
|
||||
lng: pin.longitude,
|
||||
accuracyM: pin.horizontal_accuracy ?? null,
|
||||
headingDeg: null,
|
||||
ts: Date.now(),
|
||||
source: "chat-pin",
|
||||
};
|
||||
}
|
||||
|
||||
export async function pollChatPin(
|
||||
token: string,
|
||||
options: { timeoutMs?: number; pollMs?: number; sleep?: (ms: number) => Promise<void> } = {},
|
||||
): Promise<Fix | null> {
|
||||
const timeoutMs = options.timeoutMs ?? CHAT_PIN_TIMEOUT_MS;
|
||||
const pollMs = options.pollMs ?? CHAT_PIN_POLL_MS;
|
||||
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const fix = await fetchChatPin(token);
|
||||
if (fix !== null) return fix;
|
||||
await sleep(pollMs);
|
||||
}
|
||||
return null; // bounded — returns to the denied card, never wedges AWAITING PIN
|
||||
}
|
||||
|
||||
export async function requestChatFallback(token: string): Promise<void> {
|
||||
const res = await fetch("/api/location-fallback", {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error("location-fallback-request-failed");
|
||||
}
|
||||
Reference in New Issue
Block a user