70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
/**
|
|
* 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<string, Promise<string | null>>();
|
|
|
|
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<string | null> {
|
|
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<string | null> {
|
|
try {
|
|
const r = await bff<ResolveResponse>(`/share/${encodeURIComponent(code)}`, {
|
|
revalidate: 0,
|
|
});
|
|
return r?.url ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|