Add client-side caching for public reads
This commit is contained in:
parent
59d2aa27f1
commit
25f614f4e2
|
|
@ -4,7 +4,22 @@
|
||||||
|
|
||||||
import { loadAuth } from "../auth/authStorage";
|
import { loadAuth } from "../auth/authStorage";
|
||||||
|
|
||||||
const API_BASE = "/api";
|
function apiBase() {
|
||||||
|
const raw =
|
||||||
|
(import.meta?.env?.VITE_API_BASE_URL || "")
|
||||||
|
.toString()
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
if (raw) return raw + "/api";
|
||||||
|
try {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const host = window.location.hostname || "";
|
||||||
|
if (host.startsWith("www.")) {
|
||||||
|
return `https://${host.replace(/^www\./, "")}/api`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return "/api";
|
||||||
|
}
|
||||||
|
|
||||||
function authHeaders() {
|
function authHeaders() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -17,6 +32,45 @@ function authHeaders() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseCache = new Map();
|
||||||
|
|
||||||
|
function cacheKeyFor(url, headers = {}) {
|
||||||
|
const auth = headers?.Authorization || headers?.authorization;
|
||||||
|
if (auth) return ""; // avoid caching authenticated responses
|
||||||
|
return `GET:${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneJson(value) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJsonCached(url, { ttlMs = 0, headers = {}, credentials = "include" } = {}) {
|
||||||
|
const key = cacheKeyFor(url, headers);
|
||||||
|
if (ttlMs > 0 && key) {
|
||||||
|
const hit = responseCache.get(key);
|
||||||
|
if (hit && hit.expiresAt > Date.now()) {
|
||||||
|
return cloneJson(hit.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, { credentials, headers });
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => "");
|
||||||
|
const err = new Error(text || `HTTP ${res.status}`);
|
||||||
|
err.status = res.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const data = await res.json().catch(() => null);
|
||||||
|
if (ttlMs > 0 && key) {
|
||||||
|
responseCache.set(key, { value: data, expiresAt: Date.now() + ttlMs });
|
||||||
|
}
|
||||||
|
return cloneJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function isAssetRef(value) {
|
function isAssetRef(value) {
|
||||||
return typeof value === "string" && value.startsWith("asset://");
|
return typeof value === "string" && value.startsWith("asset://");
|
||||||
|
|
@ -27,7 +81,7 @@ async function resolveAssetURLs(ids) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set("ids", ids.join(","));
|
qs.set("ids", ids.join(","));
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/assets/resolve?${qs.toString()}`, {
|
const res = await fetch(`${apiBase()}/assets/resolve?${qs.toString()}`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { ...authHeaders() },
|
headers: { ...authHeaders() },
|
||||||
});
|
});
|
||||||
|
|
@ -115,20 +169,16 @@ export async function fetchPosts(filters = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
const url = qs ? `${API_BASE}/posts?${qs}` : `${API_BASE}/posts`;
|
const url = qs ? `${apiBase()}/posts?${qs}` : `${apiBase()}/posts`;
|
||||||
|
|
||||||
const res = await fetch(url, {
|
try {
|
||||||
credentials: "include",
|
const data = await fetchJsonCached(url, { ttlMs: 15000 });
|
||||||
});
|
const items = Array.isArray(data) ? data : data?.items || [];
|
||||||
|
return await resolveAssetRefsInPosts(items);
|
||||||
if (!res.ok) {
|
} catch (err) {
|
||||||
console.warn("fetchPosts failed (treated as empty)", res.status);
|
console.warn("fetchPosts failed (treated as empty)", err?.status || err);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
const items = Array.isArray(data) ? data : data.items || [];
|
|
||||||
return await resolveAssetRefsInPosts(items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -136,27 +186,8 @@ export async function fetchPosts(filters = {}) {
|
||||||
*/
|
*/
|
||||||
export async function fetchIpLocation() {
|
export async function fetchIpLocation() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/ip-location`, {
|
const data = await fetchJsonCached(`${apiBase()}/ip-location`, { ttlMs: 10 * 60 * 1000 });
|
||||||
credentials: "include",
|
if (!data || data === "null") return null;
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.warn("fetchIpLocation: HTTP", res.status);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await res.text();
|
|
||||||
if (!text || text === "null") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(text);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("fetchIpLocation: JSON parse error", e, text);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lat = data.lat;
|
const lat = data.lat;
|
||||||
const lon = data.lon;
|
const lon = data.lon;
|
||||||
|
|
@ -190,7 +221,7 @@ export async function createPost(payload, token) {
|
||||||
headers["Authorization"] = `Bearer ${token}`;
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/post`, {
|
const res = await fetch(`${apiBase()}/post`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -235,7 +266,7 @@ export async function fetchPostPreview(url, token) {
|
||||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
else Object.assign(headers, authHeaders());
|
else Object.assign(headers, authHeaders());
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/post/preview`, {
|
const res = await fetch(`${apiBase()}/post/preview`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -260,11 +291,7 @@ export async function fetchPostById(postId) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set("id", String(postId));
|
qs.set("id", String(postId));
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post?${qs.toString()}`, {
|
const data = await fetchJsonCached(`${apiBase()}/post?${qs.toString()}`, { ttlMs: 15000 });
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const data = await res.json().catch(() => null);
|
|
||||||
if (!data || typeof data !== "object") return null;
|
if (!data || typeof data !== "object") return null;
|
||||||
const resolved = await resolveAssetRefsInPosts([data]);
|
const resolved = await resolveAssetRefsInPosts([data]);
|
||||||
return resolved && resolved[0] ? resolved[0] : data;
|
return resolved && resolved[0] ? resolved[0] : data;
|
||||||
|
|
@ -279,11 +306,7 @@ export async function fetchPostByUrl(url) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set("url", raw);
|
qs.set("url", raw);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post?${qs.toString()}`, {
|
const data = await fetchJsonCached(`${apiBase()}/post?${qs.toString()}`, { ttlMs: 15000 });
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const data = await res.json().catch(() => null);
|
|
||||||
if (!data || typeof data !== "object") return null;
|
if (!data || typeof data !== "object") return null;
|
||||||
const resolved = await resolveAssetRefsInPosts([data]);
|
const resolved = await resolveAssetRefsInPosts([data]);
|
||||||
return resolved && resolved[0] ? resolved[0] : data;
|
return resolved && resolved[0] ? resolved[0] : data;
|
||||||
|
|
@ -297,7 +320,7 @@ export async function fetchPostTruth(postId) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set("post_id", String(postId));
|
qs.set("post_id", String(postId));
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/truth?${qs.toString()}`, {
|
const res = await fetch(`${apiBase()}/post/truth?${qs.toString()}`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { ...authHeaders() },
|
headers: { ...authHeaders() },
|
||||||
});
|
});
|
||||||
|
|
@ -312,7 +335,7 @@ export async function fetchPostTruth(postId) {
|
||||||
export async function votePostTruth(postId, vote) {
|
export async function votePostTruth(postId, vote) {
|
||||||
if (!postId) throw new Error("post_id required");
|
if (!postId) throw new Error("post_id required");
|
||||||
const body = JSON.stringify({ post_id: postId, vote });
|
const body = JSON.stringify({ post_id: postId, vote });
|
||||||
const res = await fetch(`${API_BASE}/post/truth-vote`, {
|
const res = await fetch(`${apiBase()}/post/truth-vote`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -331,7 +354,7 @@ export async function updatePostVisibility(postId, payload = {}, token) {
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
const body = JSON.stringify({ post_id: postId, ...payload });
|
const body = JSON.stringify({ post_id: postId, ...payload });
|
||||||
const res = await fetch(`${API_BASE}/post/visibility`, {
|
const res = await fetch(`${apiBase()}/post/visibility`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers,
|
headers,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -352,7 +375,7 @@ export async function updatePostVisibility(postId, payload = {}, token) {
|
||||||
* payload: { username, email, password, firstName, lastName }
|
* payload: { username, email, password, firstName, lastName }
|
||||||
*/
|
*/
|
||||||
export async function registerUser(payload) {
|
export async function registerUser(payload) {
|
||||||
const res = await fetch(`${API_BASE}/signup`, {
|
const res = await fetch(`${apiBase()}/signup`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -397,15 +420,10 @@ export async function fetchSmartFeed(params = {}) {
|
||||||
if (maxLon != null) qs.set("maxLon", String(maxLon));
|
if (maxLon != null) qs.set("maxLon", String(maxLon));
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${API_BASE}/smart-feed?${qs.toString()}`;
|
const url = `${apiBase()}/smart-feed?${qs.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const data = await fetchJsonCached(url, { ttlMs: 10000 });
|
||||||
if (!res.ok) {
|
|
||||||
console.warn("fetchSmartFeed failed", res.status);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
const items = Array.isArray(data) ? data : data.results || data.items || [];
|
const items = Array.isArray(data) ? data : data.results || data.items || [];
|
||||||
return await resolveAssetRefsInPosts(items);
|
return await resolveAssetRefsInPosts(items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -428,15 +446,10 @@ export async function fetchUnifiedSearch(query, params = {}) {
|
||||||
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
||||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||||
|
|
||||||
const url = `${API_BASE}/unified-search?${qs.toString()}`;
|
const url = `${apiBase()}/unified-search?${qs.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const data = await fetchJsonCached(url, { ttlMs: 15000 });
|
||||||
if (!res.ok) {
|
|
||||||
console.warn("fetchUnifiedSearch failed", res.status);
|
|
||||||
return { posts: [], profiles: [], places: [] };
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
if (data && typeof data === "object" && !Array.isArray(data)) {
|
if (data && typeof data === "object" && !Array.isArray(data)) {
|
||||||
if (Array.isArray(data.posts)) {
|
if (Array.isArray(data.posts)) {
|
||||||
data.posts = await resolveAssetRefsInPosts(data.posts);
|
data.posts = await resolveAssetRefsInPosts(data.posts);
|
||||||
|
|
@ -464,7 +477,7 @@ export async function pollNewPosts(params = {}) {
|
||||||
if (params.category) qs.set("category", params.category);
|
if (params.category) qs.set("category", params.category);
|
||||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||||
|
|
||||||
const url = `${API_BASE}/poll?${qs.toString()}`;
|
const url = `${apiBase()}/poll?${qs.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
|
@ -498,7 +511,7 @@ export async function fetchIslands(params = {}) {
|
||||||
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
||||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||||
|
|
||||||
const url = `${API_BASE}/islands/nearby?${qs.toString()}`;
|
const url = `${apiBase()}/islands/nearby?${qs.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
|
@ -523,7 +536,7 @@ export async function fetchIslands(params = {}) {
|
||||||
*/
|
*/
|
||||||
export async function fetchIsland(islandId) {
|
export async function fetchIsland(islandId) {
|
||||||
if (!islandId) return null;
|
if (!islandId) return null;
|
||||||
const url = `${API_BASE}/islands/${encodeURIComponent(islandId)}`;
|
const url = `${apiBase()}/islands/${encodeURIComponent(islandId)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
|
@ -545,7 +558,7 @@ export async function fetchIsland(islandId) {
|
||||||
|
|
||||||
export async function fetchIslandByUsername(username) {
|
export async function fetchIslandByUsername(username) {
|
||||||
if (!username) return null;
|
if (!username) return null;
|
||||||
const url = `${API_BASE}/islands/by-username?username=${encodeURIComponent(username)}`;
|
const url = `${apiBase()}/islands/by-username?username=${encodeURIComponent(username)}`;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -566,7 +579,7 @@ export async function fetchIslandByUsername(username) {
|
||||||
|
|
||||||
export async function uploadProfileAvatar(file, opts = {}) {
|
export async function uploadProfileAvatar(file, opts = {}) {
|
||||||
if (!file) return { ok: false };
|
if (!file) return { ok: false };
|
||||||
const url = `${API_BASE}/profile/avatar`;
|
const url = `${apiBase()}/profile/avatar`;
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("file", file);
|
form.append("file", file);
|
||||||
if (opts.visibility) form.append("visibility", opts.visibility);
|
if (opts.visibility) form.append("visibility", opts.visibility);
|
||||||
|
|
@ -595,7 +608,7 @@ export async function uploadProfileAvatar(file, opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProfileUsername(username) {
|
export async function updateProfileUsername(username) {
|
||||||
const url = `${API_BASE}/profile/username`;
|
const url = `${apiBase()}/profile/username`;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -620,7 +633,7 @@ export async function updateProfileUsername(username) {
|
||||||
export async function fetchProfile(username = "") {
|
export async function fetchProfile(username = "") {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (username) qs.set("username", username);
|
if (username) qs.set("username", username);
|
||||||
const url = qs.toString() ? `${API_BASE}/profile?${qs}` : `${API_BASE}/profile`;
|
const url = qs.toString() ? `${apiBase()}/profile?${qs}` : `${apiBase()}/profile`;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -643,7 +656,7 @@ export async function fetchProfile(username = "") {
|
||||||
export async function recordPostView(postId) {
|
export async function recordPostView(postId) {
|
||||||
if (!postId) return { ok: false };
|
if (!postId) return { ok: false };
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/view`, {
|
const res = await fetch(`${apiBase()}/post/view`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -659,7 +672,7 @@ export async function recordPostView(postId) {
|
||||||
export async function togglePostLike(postId, like = true) {
|
export async function togglePostLike(postId, like = true) {
|
||||||
if (!postId) return { ok: false };
|
if (!postId) return { ok: false };
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/like`, {
|
const res = await fetch(`${apiBase()}/post/like`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -675,7 +688,7 @@ export async function togglePostLike(postId, like = true) {
|
||||||
export async function recordPostShare(postId) {
|
export async function recordPostShare(postId) {
|
||||||
if (!postId) return { ok: false };
|
if (!postId) return { ok: false };
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/share`, {
|
const res = await fetch(`${apiBase()}/post/share`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -694,7 +707,7 @@ export async function fetchPostComments(postId, limit = 20) {
|
||||||
qs.set("post_id", String(postId));
|
qs.set("post_id", String(postId));
|
||||||
if (limit) qs.set("limit", String(limit));
|
if (limit) qs.set("limit", String(limit));
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/comments?${qs.toString()}`, {
|
const res = await fetch(`${apiBase()}/post/comments?${qs.toString()}`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
|
|
@ -708,7 +721,7 @@ export async function fetchPostComments(postId, limit = 20) {
|
||||||
export async function createPostComment(postId, body) {
|
export async function createPostComment(postId, body) {
|
||||||
if (!postId || !body) return { ok: false };
|
if (!postId || !body) return { ok: false };
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/comment`, {
|
const res = await fetch(`${apiBase()}/post/comment`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
|
@ -726,7 +739,7 @@ export async function fetchPostLikeState(postId) {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
qs.set("post_id", String(postId));
|
qs.set("post_id", String(postId));
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/post/like-state?${qs.toString()}`, {
|
const res = await fetch(`${apiBase()}/post/like-state?${qs.toString()}`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { ...authHeaders() },
|
headers: { ...authHeaders() },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,51 @@
|
||||||
* - Ton search bar actuelle filtre déjà localement (0 call réseau).
|
* - Ton search bar actuelle filtre déjà localement (0 call réseau).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function apiPrefix() {
|
||||||
|
const raw =
|
||||||
|
(import.meta?.env?.VITE_API_BASE_URL || "")
|
||||||
|
.toString()
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
if (raw) return raw + "/api";
|
||||||
|
try {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const host = window.location.hostname || "";
|
||||||
|
if (host.startsWith("www.")) {
|
||||||
|
return `https://${host.replace(/^www\./, "")}/api`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return "/api";
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_PATHS = [
|
const DEFAULT_PATHS = [
|
||||||
"/api/suggestions", // NEW: Autocomplete suggestions (cities, posts, profiles)
|
"/suggestions", // NEW: Autocomplete suggestions (cities, posts, profiles)
|
||||||
"/api/unified-search", // Unified search (posts + profiles + places)
|
"/unified-search", // Unified search (posts + profiles + places)
|
||||||
"/api/search", // Fallback: Posts only
|
"/search", // Fallback: Posts only
|
||||||
"/api/reco/search",
|
"/reco/search",
|
||||||
"/api/reco/suggest",
|
"/reco/suggest",
|
||||||
"/api/recommendations/search",
|
"/recommendations/search",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const searchCache = new Map();
|
||||||
|
|
||||||
|
function cacheGet(query) {
|
||||||
|
const key = query.toLowerCase().trim();
|
||||||
|
const hit = searchCache.get(key);
|
||||||
|
if (!hit) return null;
|
||||||
|
if (hit.expiresAt <= Date.now()) {
|
||||||
|
searchCache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return hit.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheSet(query, value, ttlMs) {
|
||||||
|
const key = query.toLowerCase().trim();
|
||||||
|
if (!key) return;
|
||||||
|
searchCache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalise un item venant du core-api.
|
* Normalise un item venant du core-api.
|
||||||
* On reste permissif: Rico / core-api peut renvoyer plusieurs shapes.
|
* On reste permissif: Rico / core-api peut renvoyer plusieurs shapes.
|
||||||
|
|
@ -70,6 +106,8 @@ function normalizeOne(r) {
|
||||||
async function tryFetchAnyPath(q, { signal } = {}) {
|
async function tryFetchAnyPath(q, { signal } = {}) {
|
||||||
const query = (q || "").toString().trim();
|
const query = (q || "").toString().trim();
|
||||||
if (!query) return [];
|
if (!query) return [];
|
||||||
|
const cached = cacheGet(query);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
// Permet override runtime si tu veux:
|
// Permet override runtime si tu veux:
|
||||||
// window.__SW_RECO_SEARCH_PATH = "/api/reco/search"
|
// window.__SW_RECO_SEARCH_PATH = "/api/reco/search"
|
||||||
|
|
@ -78,11 +116,13 @@ async function tryFetchAnyPath(q, { signal } = {}) {
|
||||||
? String(window.__SW_RECO_SEARCH_PATH)
|
? String(window.__SW_RECO_SEARCH_PATH)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const prefix = apiPrefix();
|
||||||
const paths = override ? [override, ...DEFAULT_PATHS] : DEFAULT_PATHS;
|
const paths = override ? [override, ...DEFAULT_PATHS] : DEFAULT_PATHS;
|
||||||
|
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
try {
|
try {
|
||||||
const url = `${p}?q=${encodeURIComponent(query)}`;
|
const base = p.startsWith("http") ? p : `${prefix}${p}`;
|
||||||
|
const url = `${base}?q=${encodeURIComponent(query)}`;
|
||||||
console.log('[recoSearch] Trying:', url);
|
console.log('[recoSearch] Trying:', url);
|
||||||
const res = await fetch(url, { credentials: "include", signal });
|
const res = await fetch(url, { credentials: "include", signal });
|
||||||
|
|
||||||
|
|
@ -106,7 +146,9 @@ async function tryFetchAnyPath(q, { signal } = {}) {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log('[recoSearch] Success! Returning', arr.length, 'results from', p);
|
console.log('[recoSearch] Success! Returning', arr.length, 'results from', p);
|
||||||
return arr.map(normalizeOne).filter(Boolean);
|
const out = arr.map(normalizeOne).filter(Boolean);
|
||||||
|
cacheSet(query, out, 30 * 1000);
|
||||||
|
return out;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Abort => stop net
|
// Abort => stop net
|
||||||
if (e && (e.name === "AbortError" || e.code === 20)) {
|
if (e && (e.name === "AbortError" || e.code === 20)) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue