/** * Short link client. * * - `createShortLink` runs in the browser and POSTs to `/api/share`, a Next.js * route handler that proxies to the BFF. We can't call the BFF directly * from the browser because `BFF_URL` is not exposed to the client bundle * (no `NEXT_PUBLIC_` prefix). Same-origin proxy also avoids CORS. * * - `resolveShortLink` is only called from the `/share/[code]` server * component, so it can use `bff()` directly with the server-side env var. */ import { bff } from "@/lib/bff"; // Module-scoped cache: a single user shouldn't generate a fresh code every // time they re-click Share on the same selection. Keyed by the relative path // (including ?hl=…), this returns the same promise for repeat calls. const createCache = new Map>(); type ResolveResponse = { url: string }; /** * Create a short code for a same-site relative path (`/books/bg/18/66?hl=…`). * Returns `null` on any failure — caller should fall back to the long URL. */ export function createShortLink(longPath: string): Promise { const cached = createCache.get(longPath); if (cached) return cached; const promise = fetch("/api/share", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ url: longPath }), }) .then(async (res) => { if (!res.ok) return null; const json = await res.json().catch(() => null); const code = json?.data?.code; return typeof code === "string" ? code : null; }) .catch(() => null); createCache.set(longPath, promise); // If the request fails, evict so the next click can retry instead of // returning the cached null forever. promise.then((code) => { if (code === null) createCache.delete(longPath); }); return promise; } /** * Resolve a short code to its full long path. Returns `null` when the code is * not found, expired, disabled, or the BFF is unreachable. */ export async function resolveShortLink(code: string): Promise { try { const r = await bff(`/share/${encodeURIComponent(code)}`, { revalidate: 0, }); return r?.url ?? null; } catch { return null; } }