mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
add: more commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useRef, useState } from "react";
|
||||
import api from "@/utils/api.util";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -20,6 +20,16 @@ const PAGINATION_INIT = {
|
||||
hasNextPage: false,
|
||||
};
|
||||
|
||||
// No Redis yet — these caches are plain in-memory (per browser tab, cleared on
|
||||
// refresh) to absorb the repeated open/close traffic pickers like
|
||||
// AssetPickerSheet generate against Postgres and the media-token endpoint.
|
||||
const LIST_CACHE_TTL_MS = 20_000; // short: just enough to survive rapid open/close flapping
|
||||
const MEDIA_TOKEN_TTL_MS = 30 * 60 * 1000; // mirrors TOKEN_TTL_SEC in media.controller.js
|
||||
const MEDIA_TOKEN_REFRESH_MARGIN_MS = 2 * 60 * 1000; // re-mint a bit before real expiry
|
||||
|
||||
const cacheKeyFor = (scope, { page, limit, filters, sort }) =>
|
||||
`${scope}:${JSON.stringify({ page, limit, filters, sort })}`;
|
||||
|
||||
export function AssetsProvider({ children }) {
|
||||
const [assets, setAssets] = useState([]);
|
||||
const [attributes, setAttributes] = useState([]);
|
||||
@@ -27,6 +37,32 @@ export function AssetsProvider({ children }) {
|
||||
const [selectedAsset, setSelectedAsset] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// { [asset_id]: { token, thumbnail_url, issuedAt } } — shared across every
|
||||
// picker instance so tokens survive sheet open/close for their full TTL.
|
||||
const [mediaTokens, setMediaTokens] = useState({});
|
||||
const mediaTokensRef = useRef({});
|
||||
const listCacheRef = useRef(new Map());
|
||||
|
||||
const invalidateListCache = () => listCacheRef.current.clear();
|
||||
|
||||
// Seeds mediaTokens from stream_token/thumbnail_url fields the backend now
|
||||
// embeds directly in S3 rows of GET /admin/assets — so getMediaTokens (called
|
||||
// right after fetchAssets by pickers/tables) finds them already cached and
|
||||
// skips the batch round-trip instead of re-requesting tokens it just got.
|
||||
const seedMediaTokensFromRows = (rows = []) => {
|
||||
const issuedAt = Date.now();
|
||||
const next = {};
|
||||
for (const row of rows) {
|
||||
if (row.stream_token) {
|
||||
next[String(row.asset_id)] = { token: row.stream_token, thumbnail_url: row.thumbnail_url ?? null, issuedAt };
|
||||
}
|
||||
}
|
||||
if (Object.keys(next).length) {
|
||||
mediaTokensRef.current = { ...mediaTokensRef.current, ...next };
|
||||
setMediaTokens(mediaTokensRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const request = useCallback(async (fn) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -41,9 +77,22 @@ export function AssetsProvider({ children }) {
|
||||
}, []);
|
||||
|
||||
// ─── GET /api/admin/assets ────────────────────────────────────────────────
|
||||
// Cached per (page, limit, filters, sort) for LIST_CACHE_TTL_MS so toggling
|
||||
// a picker like AssetPickerSheet open/closed doesn't re-hit Postgres for the
|
||||
// same query within the TTL window. Pass force: true to bypass the cache.
|
||||
const fetchAssets = useCallback(
|
||||
({ page = 1, limit = 10, filters = [], sort = [] } = {}) =>
|
||||
request(async () => {
|
||||
({ page = 1, limit = 10, filters = [], sort = [], force = false } = {}) => {
|
||||
const key = cacheKeyFor("assets", { page, limit, filters, sort });
|
||||
const cached = listCacheRef.current.get(key);
|
||||
if (!force && cached && Date.now() - cached.fetchedAt < LIST_CACHE_TTL_MS) {
|
||||
setAssets(cached.assets);
|
||||
setPagination(cached.pagination);
|
||||
setAttributes(cached.attributes);
|
||||
seedMediaTokensFromRows(cached.assets);
|
||||
return Promise.resolve(cached.raw);
|
||||
}
|
||||
|
||||
return request(async () => {
|
||||
const { data } = await api.get("/admin/assets", {
|
||||
params: {
|
||||
page, limit,
|
||||
@@ -57,12 +106,53 @@ export function AssetsProvider({ children }) {
|
||||
setAssets(result?.data ?? []);
|
||||
setPagination(result?.pagination ?? PAGINATION_INIT);
|
||||
setAttributes(result.attributes);
|
||||
seedMediaTokensFromRows(result?.data);
|
||||
|
||||
listCacheRef.current.set(key, {
|
||||
assets: result?.data ?? [],
|
||||
pagination: result?.pagination ?? PAGINATION_INIT,
|
||||
attributes: result.attributes,
|
||||
raw: data.data,
|
||||
fetchedAt: Date.now(),
|
||||
});
|
||||
|
||||
return data.data;
|
||||
}),
|
||||
});
|
||||
},
|
||||
[request]
|
||||
);
|
||||
|
||||
// ─── POST /api/admin/media/tokens (batch) ────────────────────────────────
|
||||
// Skips any asset_id whose cached token is still within its TTL (minus a
|
||||
// safety margin) instead of re-minting a fresh JWT/presigned URL every time
|
||||
// a picker reopens. Shared across all picker instances via context state.
|
||||
const getMediaTokens = useCallback((assetIds = []) => {
|
||||
const now = Date.now();
|
||||
const missing = assetIds
|
||||
.map(String)
|
||||
.filter((id) => {
|
||||
const cached = mediaTokensRef.current[id];
|
||||
return !cached || (now - cached.issuedAt) > (MEDIA_TOKEN_TTL_MS - MEDIA_TOKEN_REFRESH_MARGIN_MS);
|
||||
});
|
||||
|
||||
if (!missing.length) return Promise.resolve(mediaTokensRef.current);
|
||||
|
||||
return api.post("/admin/media/tokens", { asset_ids: missing }).then(({ data }) => {
|
||||
const tokens = data.data?.tokens ?? {};
|
||||
const thumbnails = data.data?.thumbnails ?? {};
|
||||
const issuedAt = Date.now();
|
||||
|
||||
const next = {};
|
||||
for (const [id, token] of Object.entries(tokens)) {
|
||||
next[id] = { token, thumbnail_url: thumbnails[id] ?? null, issuedAt };
|
||||
}
|
||||
|
||||
mediaTokensRef.current = { ...mediaTokensRef.current, ...next };
|
||||
setMediaTokens(mediaTokensRef.current);
|
||||
return mediaTokensRef.current;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ─── GET /api/admin/assets/:assetId ───────────────────────────────────────
|
||||
const fetchAsset = useCallback(
|
||||
(assetId) =>
|
||||
@@ -110,6 +200,7 @@ export function AssetsProvider({ children }) {
|
||||
const asset = res.data?.data?.data ?? null;
|
||||
if (asset) {
|
||||
setAssets((prev) => [asset, ...prev]);
|
||||
invalidateListCache();
|
||||
toast.success("Asset uploaded successfully.");
|
||||
}
|
||||
return res.data;
|
||||
@@ -135,6 +226,7 @@ export function AssetsProvider({ children }) {
|
||||
if (asset) {
|
||||
setAssets((prev) => prev.map((a) => (a.asset_id === assetId ? asset : a)));
|
||||
setSelectedAsset(asset);
|
||||
invalidateListCache();
|
||||
toast.success("Asset updated successfully.");
|
||||
}
|
||||
return res.data;
|
||||
@@ -151,6 +243,7 @@ export function AssetsProvider({ children }) {
|
||||
});
|
||||
setAssets((prev) => prev.filter((a) => a.asset_id !== assetId));
|
||||
setSelectedAsset((prev) => (prev?.asset_id === assetId ? null : prev));
|
||||
invalidateListCache();
|
||||
toast.success("Asset archived.");
|
||||
return res.data;
|
||||
}),
|
||||
@@ -165,6 +258,7 @@ export function AssetsProvider({ children }) {
|
||||
data: { ids, deletedBy },
|
||||
});
|
||||
setAssets((prev) => prev.filter((a) => !ids.includes(a.asset_id)));
|
||||
invalidateListCache();
|
||||
toast.success(`${ids.length} asset(s) archived.`);
|
||||
return res.data;
|
||||
}),
|
||||
@@ -179,6 +273,7 @@ export function AssetsProvider({ children }) {
|
||||
const asset = res.data?.data?.data ?? null;
|
||||
if (asset) {
|
||||
setAssets((prev) => prev.filter((a) => a.asset_id !== assetId));
|
||||
invalidateListCache();
|
||||
toast.success("Asset restored.");
|
||||
}
|
||||
return res.data;
|
||||
@@ -192,6 +287,7 @@ export function AssetsProvider({ children }) {
|
||||
request(async () => {
|
||||
const res = await api.patch("/admin/assets/bulk-restore", { ids });
|
||||
setAssets((prev) => prev.filter((a) => !ids.includes(a.asset_id)));
|
||||
invalidateListCache();
|
||||
toast.success(`${ids.length} asset(s) restored.`);
|
||||
return res.data;
|
||||
}),
|
||||
@@ -215,6 +311,8 @@ export function AssetsProvider({ children }) {
|
||||
pagination,
|
||||
selectedAsset,
|
||||
loading,
|
||||
mediaTokens,
|
||||
getMediaTokens,
|
||||
setPagination,
|
||||
setSelectedAsset,
|
||||
fetchAssets,
|
||||
|
||||
Reference in New Issue
Block a user