This commit is contained in:
2026-04-27 14:23:19 +08:00
parent 09d3fe59b3
commit fa2dbb310b
75 changed files with 7363 additions and 1487 deletions

View File

@@ -6,6 +6,17 @@ export type AssetReadUrlRequest = {
expireSeconds?: number;
};
type AssetReadUrlResolveOptions = {
signal?: AbortSignal;
expireSeconds?: number;
/**
* 图片内容可能在同一路径下被重新写入。
* 这时需要显式跳过本地签名缓存,并在最终 URL 上追加一次性参数,
* 避免结果页仍命中旧签名地址或浏览器图片缓存。
*/
refreshKey?: string | number | null;
};
export type AssetReadUrlResponse = {
read?: {
objectKey?: string;
@@ -100,21 +111,26 @@ function shouldReuseCachedReadUrlFailure(
export async function getSignedAssetReadUrl(
request: AssetReadUrlRequest,
signal?: AbortSignal,
options: {
bypassCache?: boolean;
} = {},
) {
const cacheKey = buildCacheKey(request);
const cached = cacheKey ? signedReadUrlCache.get(cacheKey) : undefined;
const bypassCache = options.bypassCache === true;
const cached =
!bypassCache && cacheKey ? signedReadUrlCache.get(cacheKey) : undefined;
if (cached && shouldReuseCachedReadUrl(cached)) {
return cached.signedUrl;
}
const cachedFailure = cacheKey
const cachedFailure = !bypassCache && cacheKey
? signedReadUrlFailureCache.get(cacheKey)
: undefined;
if (cachedFailure && shouldReuseCachedReadUrlFailure(cachedFailure)) {
throw new Error('资源不存在或暂时不可读取');
}
if (cacheKey) {
if (cacheKey && !bypassCache) {
const pendingRequest = pendingSignedReadUrlRequests.get(cacheKey);
if (pendingRequest) {
return pendingRequest;
@@ -178,26 +194,48 @@ export async function getSignedAssetReadUrl(
}
})();
if (cacheKey) {
if (cacheKey && !bypassCache) {
pendingSignedReadUrlRequests.set(cacheKey, requestPromise);
}
try {
return await requestPromise;
} finally {
if (cacheKey) {
if (cacheKey && !bypassCache) {
pendingSignedReadUrlRequests.delete(cacheKey);
}
}
}
function appendCacheBustParam(
url: string,
refreshKey: string | number | null | undefined,
) {
const normalizedRefreshKey =
refreshKey === null || refreshKey === undefined
? ''
: String(refreshKey).trim();
if (!normalizedRefreshKey) {
return url;
}
try {
const parsedUrl = new URL(url, globalThis.location?.origin ?? 'http://localhost');
parsedUrl.searchParams.set('_v', normalizedRefreshKey);
if (/^(?:https?:)?\/\//u.test(url)) {
return parsedUrl.toString();
}
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
} catch {
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}_v=${encodeURIComponent(normalizedRefreshKey)}`;
}
}
// 兼容层:普通 http(s)/data/blob 路径原样返回;历史 generated-* 路径自动换签名读 URL。
export async function resolveAssetReadUrl(
source: string | null | undefined,
options: {
signal?: AbortSignal;
expireSeconds?: number;
} = {},
options: AssetReadUrlResolveOptions = {},
) {
const value = source?.trim() ?? '';
if (!value) {
@@ -209,20 +247,25 @@ export async function resolveAssetReadUrl(
value.startsWith('data:') ||
value.startsWith('blob:')
) {
return value;
return appendCacheBustParam(value, options.refreshKey);
}
if (isGeneratedLegacyPath(value)) {
return getSignedAssetReadUrl(
const signedUrl = await getSignedAssetReadUrl(
{
legacyPublicPath: value,
expireSeconds: options.expireSeconds,
},
options.signal,
{
bypassCache:
options.refreshKey !== null && options.refreshKey !== undefined,
},
);
return appendCacheBustParam(signedUrl, options.refreshKey);
}
return value;
return appendCacheBustParam(value, options.refreshKey);
}
export function clearSignedAssetReadUrlCache() {