diff --git a/apps/admin-web/src/components/AdminEditorAssetMedia.test.tsx b/apps/admin-web/src/components/AdminEditorAssetMedia.test.tsx index dcebe3244..419977ef0 100644 --- a/apps/admin-web/src/components/AdminEditorAssetMedia.test.tsx +++ b/apps/admin-web/src/components/AdminEditorAssetMedia.test.tsx @@ -34,6 +34,7 @@ function createDeferred() { test('角色动作预览跨窗口回播时复用父级换签缓存并等待目标帧就绪', async () => { vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-07-04T10:50:00Z')); const frameObjectKeys = [ 'generated-animations/editor/task-cache/frame00.png', 'generated-animations/editor/task-cache/frame01.png', @@ -149,6 +150,114 @@ test('角色动作预览跨窗口回播时复用父级换签缓存并等待目 ).toBeLessThanOrEqual(3); }); +test('角色动作预览打开超过五分钟后回绕播放会在过期窗口内自动换签', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2099-01-01T00:00:00Z')); + const frameObjectKeys = Array.from( + { length: 5 }, + (_, index) => `generated-animations/editor/task-expiry/frame0${index}.png`, + ); + const requestCounts = new Map(); + vi.mocked(getAdminAssetReadUrl).mockImplementation((_token, request) => { + const objectKey = request.objectKey ?? ''; + const requestCount = (requestCounts.get(objectKey) ?? 0) + 1; + requestCounts.set(objectKey, requestCount); + return Promise.resolve({ + read: { + objectKey, + signedUrl: `https://signed.example.com/v${requestCount}/${objectKey}`, + expiresAt: + requestCount === 1 ? '2099-01-01T00:05:00Z' : '2099-01-01T00:10:00Z', + }, + }); + }); + + render( + ({ + imageSrc: `/${objectKey}`, + objectKey, + width: 192, + height: 256, + })), + imageSequenceDurationMs: 5_000, + }} + token="admin-token" + onClose={vi.fn()} + />, + ); + + const dialog = screen.getByRole('dialog', { name: '素材预览' }); + fireEvent.click(within(dialog).getByRole('button', { name: '暂停角色动作' })); + await act(async () => { + await vi.advanceTimersByTimeAsync(120); + }); + loadMountedFrames(dialog); + + for (let index = 0; index < frameObjectKeys.length; index += 1) { + fireEvent.click( + within(dialog).getByRole('button', { name: '播放角色动作' }), + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(1_000); + }); + fireEvent.click( + within(dialog).getByRole('button', { name: '暂停角色动作' }), + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(120); + }); + loadMountedFrames(dialog); + } + expect(within(dialog).getByText('1/5')).toBeTruthy(); + frameObjectKeys.forEach((objectKey) => { + expect(requestCounts.get(objectKey)).toBe(1); + }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(300_001); + }); + loadMountedFrames(dialog); + expect(resolveVisibleFrameSrc(dialog)).toContain('/v2/'); + + for (let index = 0; index < frameObjectKeys.length; index += 1) { + fireEvent.click( + within(dialog).getByRole('button', { name: '播放角色动作' }), + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(1_000); + }); + fireEvent.click( + within(dialog).getByRole('button', { name: '暂停角色动作' }), + ); + await act(async () => { + await vi.advanceTimersByTimeAsync(120); + }); + loadMountedFrames(dialog); + } + + expect(within(dialog).getByText('1/5')).toBeTruthy(); + expect(resolveVisibleFrameSrc(dialog)).toBe( + `https://signed.example.com/v2/${frameObjectKeys[0]}`, + ); + frameObjectKeys.forEach((objectKey) => { + expect(requestCounts.get(objectKey)).toBe(2); + }); + expect(within(dialog).queryByText(/帧加载失败/u)).toBeNull(); +}); + +function loadMountedFrames(dialog: HTMLElement) { + dialog + .querySelectorAll('.admin-asset-query-sequence-frame') + .forEach((frame) => fireEvent.load(frame)); +} + function resolveFrameByObjectKey(dialog: HTMLElement, objectKey: string) { return Array.from( dialog.querySelectorAll( diff --git a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx index eb36d2b1a..dc14da339 100644 --- a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx +++ b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx @@ -1,6 +1,11 @@ import { Pause, Play, X } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + parseSignedReadUrlExpiresAtMs, + shouldReuseSignedReadUrlCacheEntry, + SIGNED_READ_URL_CACHE_SAFETY_WINDOW_MS, +} from '../../../../packages/shared/src/utils/signedReadUrlCache'; import type { AdminAssetReadUrlResponse } from '../api/adminApiClient'; import { getAdminAssetReadUrl, isAdminApiError } from '../api/adminApiClient'; import type { AdminEditorImageSequenceFramePayload } from '../api/adminApiTypes'; @@ -337,6 +342,35 @@ function AdminEditorImageSequencePreview({ resolveFrames(mountedFrameItems); }, [mountedFrameItems, resolveFrames]); + useEffect(() => { + const refreshAtMs = mountedFrameItems.reduce( + (earliest, item) => { + const cached = frameCache.get(item.key); + if ( + !cached || + cached.expiresAtMs === null || + cached.status === 'resolving' || + cached.status === 'refreshing' || + cached.status === 'failed' + ) { + return earliest; + } + const candidate = + cached.expiresAtMs - SIGNED_READ_URL_CACHE_SAFETY_WINDOW_MS; + return earliest === null || candidate < earliest ? candidate : earliest; + }, + null as number | null, + ); + if (refreshAtMs === null) { + return undefined; + } + const timer = window.setTimeout( + () => resolveFrames(mountedFrameItems), + Math.min(2_147_483_647, Math.max(0, refreshAtMs - Date.now())), + ); + return () => window.clearTimeout(timer); + }, [frameCache, mountedFrameItems, resolveFrames]); + useEffect(() => { if (!currentFrameKey || !failedFrameKeys.has(currentFrameKey)) { return; @@ -360,7 +394,7 @@ function AdminEditorImageSequencePreview({ return; } if ( - frameCache.get(currentFrameKey)?.status !== 'ready' && + !isAdminImageSequenceFrameReady(frameCache.get(currentFrameKey)) && visibleFrameKey ) { return; @@ -407,7 +441,7 @@ function AdminEditorImageSequencePreview({ frameKey={item.key} resolvedUrl={cachedFrame?.resolvedUrl ?? ''} visible={item.key === visibleFrameKey} - ready={cachedFrame?.status === 'ready'} + ready={isAdminImageSequenceFrameReady(cachedFrame)} onReady={markFrameReady} onFailed={markFrameFailed} /> @@ -446,8 +480,9 @@ type AdminImageSequenceFrameItem = { }; type AdminImageSequenceFrameCacheEntry = { + expiresAtMs: number | null; resolvedUrl: string; - status: 'resolving' | 'loading' | 'ready' | 'failed'; + status: 'resolving' | 'refreshing' | 'loading' | 'ready' | 'failed'; }; type AdminImageSequenceFrameCacheState = { @@ -524,36 +559,41 @@ function useAdminImageSequenceFrameCache( const resolveFrames = useCallback( (frameItemsToResolve: ReadonlyArray) => { for (const item of frameItemsToResolve) { - if ( - frameCache.has(item.key) || - resolutionControllersRef.current.has(item.key) - ) { + const cached = frameCache.get(item.key); + if (resolutionControllersRef.current.has(item.key)) { continue; } + if (cached && shouldReuseAdminImageSequenceFrameCacheEntry(cached)) { + continue; + } + const refreshReadyFrame = isAdminImageSequenceFrameReady(cached); const controller = new AbortController(); resolutionControllersRef.current.set(item.key, controller); - updateFrameCacheEntry(item.key, () => ({ - resolvedUrl: '', - status: 'resolving', - })); + updateFrameCacheEntry(item.key, (current) => + refreshReadyFrame && current?.resolvedUrl + ? { ...current, status: 'refreshing' } + : { + expiresAtMs: null, + resolvedUrl: '', + status: 'resolving', + }, + ); void resolveAdminAssetUrl( token, item.frame.imageSrc, item.frame.objectKey, controller.signal, ) - .then((resolvedUrl) => { - if (!resolvedUrl) { - throw new Error('素材读取地址为空'); - } + .then((resolved) => { updateFrameCacheEntry(item.key, () => ({ - resolvedUrl, - status: 'loading', + ...resolved, + status: refreshReadyFrame ? 'refreshing' : 'loading', })); }) .catch(() => { if (!controller.signal.aborted) { updateFrameCacheEntry(item.key, () => ({ + expiresAtMs: null, resolvedUrl: '', status: 'failed', })); @@ -585,6 +625,7 @@ function useAdminImageSequenceFrameCache( resolutionControllersRef.current.get(frameKey)?.abort(); resolutionControllersRef.current.delete(frameKey); updateFrameCacheEntry(frameKey, () => ({ + expiresAtMs: null, resolvedUrl: '', status: 'failed', })); @@ -615,6 +656,27 @@ function useAdminImageSequenceFrameCache( }; } +function isAdminImageSequenceFrameReady( + cached: AdminImageSequenceFrameCacheEntry | undefined, +) { + return cached?.status === 'ready' || cached?.status === 'refreshing'; +} + +function shouldReuseAdminImageSequenceFrameCacheEntry( + cached: AdminImageSequenceFrameCacheEntry, +) { + if (cached.status === 'failed') { + return false; + } + if (cached.expiresAtMs === null) { + return true; + } + return shouldReuseSignedReadUrlCacheEntry({ + signedUrl: cached.resolvedUrl, + expiresAtMs: cached.expiresAtMs, + }); +} + function findNextAvailableFrameIndex( frameItems: ReadonlyArray<{ key: string }>, failedFrameKeys: ReadonlySet, @@ -910,7 +972,10 @@ async function resolveAdminAssetUrl( ? normalizedImageSrc : resolveAdminGeneratedLegacyPathFromUrl(normalizedImageSrc); if (!normalizedObjectKey && !normalizedLegacyPublicPath) { - return normalizedImageSrc; + return { + expiresAtMs: null, + resolvedUrl: normalizedImageSrc, + }; } let retryIndex = 0; @@ -932,7 +997,18 @@ async function resolveAdminAssetUrl( if (signal.aborted) { throw new DOMException('The operation was aborted.', 'AbortError'); } - return resolveAdminAssetReadSignedUrl(response); + const read = response.read ?? response; + const resolvedUrl = resolveAdminAssetReadSignedUrl(response); + const expiresAt = + typeof read.expiresAt === 'string' ? read.expiresAt.trim() : ''; + const expiresAtMs = parseSignedReadUrlExpiresAtMs(expiresAt); + if (!resolvedUrl) { + throw new Error('素材读取地址为空'); + } + if (expiresAtMs <= Date.now()) { + throw new Error('素材读取地址过期时间无效'); + } + return { expiresAtMs, resolvedUrl }; } catch (error: unknown) { if (signal.aborted) { throw error; diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 27c8996f1..ada852eb6 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -69,7 +69,7 @@ npm run check:server-rs-ddd - 现役 runtime 前缀公共能力:`GET /api/runtime/frontend-config`、鉴权的 `GET/PUT /api/runtime/settings` 以及鉴权的 `/api/runtime/external-generation/*`。这些分别承载非敏感前端开关、账号级公共设置和外部生成队列观测 / 确认,路径保留 `runtime` 前缀不代表旧玩法运行态恢复。`/api/runtime/frontend-config` 由 `api-server` 从运行时环境变量下发非敏感 UI 开关;画板右侧 Agent 入口由 `GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR` 控制,默认关闭。 - 公共设置数据链:`GET/PUT /api/runtime/settings` 从 access token 取得 `user_id`,经 `spacetime-client` facade 调用 `get_runtime_setting_or_default` / `upsert_runtime_setting_and_return`,读写现役 `runtime_setting` 表的 `music_volume` 和 `platform_theme`。该表不是历史数据壳,也不得为保留它而恢复任何旧存档、游玩历史或玩法 settings 路由。 - 后台素材查询:`GET /admin/api/editor-assets` 通过 `admin_list_editor_assets_and_return` 后台只读 procedure 读取私有账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`keyword`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;`ownerUserId` 接受内部用户 ID 或精确陶泥号,精确 `SY-*` keyword 也会在调用 procedure 前经认证服务解析成内部 `user_id`,未知陶泥号直接返回空列表。带 owner 条件时 procedure 直接走 `by_editor_asset_owner_user_id`,再按任务分组并以最终产物父项分页;响应中每个父项通过 `children` 携带可展开的中间产物,子项各占一行而不单独占分页名额。手动重拆图集保留真实 `editor-atlas-split-*` `task_id`;来源任务只能通过私有 provenance 由服务端生成账号素材的 source resource / asset object / Object Key 推导,再写入 `group_task_id`,不能信任素材库兼容推断值或普通资源创建接口可提交的 `task_id/asset_kind`;procedure 只有当前 editor generation runtime service identity 才能写入归组与完成事实。没有可信来源的现代拆分显式归到自身任务,不进入 legacy 回溯。每个切片同时写入 `group_task_expected_asset_count`,全部预期 asset ID 成功落库并逐行校验后写入不可逆 cohort 完成事实;read model 只让具有完成事实的一个拆分批次并入原图集父项,用户后来删除单片不会改变初始完成状态,部分失败批次与后续重复拆分按真实任务独立分页,避免残缺批次抢占根任务、单组无限增长或丢素材。历史行兼容沿项目资源链回溯,删除项目时只固化直接引用待删资源且尚未固化的历史行,持久化真实来源任务,不把有界展示 ID 反写覆盖来源。父项返回任务生成器和任务总成本,子项返回阶段生成器和阶段成本。provider 原图 / 角色动作预览承载模型生成成本,抠图、逐帧处理、透明图集和切片成本为 0;中间产物使用所属任务真实 `asset_kind`,不再新写 `editor_green_screen_source`,历史旧值只在 read model 中按 Object Key 映射为 `character`、`icon-spritesheet` 或 `character-animation` 并把旧任务成本只读归回 provider 原始产物。游标固定由父任务微秒时间和素材 ID 组成,API 必须同时识别整数微秒、`seconds.microsZ` 与 RFC3339 时间文本;内部时间无法编码时返回服务错误,不得静默返回 `nextCursor = null`。接口只用于查询,不提供分类筛选,也不执行精选审核、返还或展示状态修改,不通过后台 SQL 直查私有表。图片放大、视频和音频预览统一调用仅后台管理员会话可访问的 `GET /admin/api/assets/read-url`;该入口仅为预览允许后台跨 owner 签名,不改变主站 `/api/assets/read-*` 或 External API 的 owner 边界。成功换签后必须写入 `event_key = admin_asset_read_url` 的 `tracking_event`,记录管理员 subject、Object Key / legacy path 和有效期,不记录 signed URL。 -- 后台精选审核:`GET /admin/api/editor-showcase/assets` 的素材 payload 与素材查询共同返回稳定媒体引用,并只透传底层正式 `thumbnailSrc`、`imageSequenceFrames` 与 `imageSequenceDurationMs`;缺少正式动作字段的存量数据必须先经角色动作规范化 procedure 迁移,不在 admin mapper 回读旧 `generationInputs`。两页前端共用同一缩略图、媒体类型、管理员换签和预览弹窗:动作列表缩略图只换签首帧,打开弹窗后才逐帧换签并按完整时长播放,不得在精选审核中复制一套立即全量换签或把绝对 OSS 私有地址直接交给浏览器的简化实现。角色动作弹窗由预览父组件持有当前序列有界的 signed URL、解析中、已加载和失败状态,帧元素只消费缓存 URL;缓存最多覆盖当前序列帧,DOM 仍最多挂载当前可见窗口的 3 帧,切换序列、管理员 token 或关闭弹窗时中止在途换签并释放缓存。已经离开 DOM 的帧回播时复用同一缓存 URL 与 readiness,不得重新换签或在目标帧尚未加载完成时提前隐藏上一可见帧。 +- 后台精选审核:`GET /admin/api/editor-showcase/assets` 的素材 payload 与素材查询共同返回稳定媒体引用,并只透传底层正式 `thumbnailSrc`、`imageSequenceFrames` 与 `imageSequenceDurationMs`;缺少正式动作字段的存量数据必须先经角色动作规范化 procedure 迁移,不在 admin mapper 回读旧 `generationInputs`。两页前端共用同一缩略图、媒体类型、管理员换签和预览弹窗:动作列表缩略图只换签首帧,打开弹窗后才逐帧换签并按完整时长播放,不得在精选审核中复制一套立即全量换签或把绝对 OSS 私有地址直接交给浏览器的简化实现。角色动作弹窗由预览父组件持有当前序列有界的 signed URL、`expiresAt`、解析中、已加载和失败状态,帧元素只消费缓存 URL;缓存沿用主站 signed URL 读取缓存的 30 秒过期安全窗口,当前挂载帧进入窗口时自动换签,已经离开 DOM 的帧只在未临近过期时复用 URL,过期后再次进入窗口必须先换签。缓存最多覆盖当前序列帧,DOM 仍最多挂载当前可见窗口的 3 帧,切换序列、管理员 token 或关闭弹窗时中止在途换签并释放缓存;换签期间继续展示上一已就绪帧,不得在新地址尚未加载完成时提前隐藏可见帧。 现役路由只以 `app.rs::build_router` 实际 `.merge(...)` 的 module 和支付回调为准。`modules/` 中仍保留但未被 `app.rs` 声明 / 合并的 RPG、拼图、Match3D、敲木鱼、方洞、视觉小说、大鱼、跳一跳、汪汪声浪、儿童向、旧公开作品与 `play_flow` 源码都是历史追溯材料,不得由新的 `.merge(...)`、handler 转发或兼容 router 重新挂载。`/api/creation-entry/config`、`/admin/api/creation-entry/config*`、旧 `/api/creation/*`、旧 `/api/runtime//*`、旧存档 / 游玩历史 / 公开作品路由均不是现役 API。新增公共路由仍必须经 `app.rs` 的统一中间件、鉴权、背压、埋点与可观测边界。 ### 图片画布 Agent 对话 diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a6f0f50f8..e9cc3ae8b 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -50,5 +50,6 @@ export type * from './contracts/visualNovel'; export * from './http'; export * from './llm/narrativeLanguage'; export * from './llm/parsers'; +export * from './utils/signedReadUrlCache'; // should not export component, instead, they should be ref by relative path diff --git a/packages/shared/src/utils/signedReadUrlCache.test.ts b/packages/shared/src/utils/signedReadUrlCache.test.ts new file mode 100644 index 000000000..89dcfe8ec --- /dev/null +++ b/packages/shared/src/utils/signedReadUrlCache.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'vitest'; + +import { + parseSignedReadUrlExpiresAtMs, + shouldReuseSignedReadUrlCacheEntry, +} from './signedReadUrlCache'; + +describe('signedReadUrlCache', () => { + test('只在签名地址距离过期超过安全窗口时复用', () => { + const nowMs = Date.parse('2099-01-01T00:00:00Z'); + const entry = { + signedUrl: 'https://signed.example.com/frame.png', + expiresAtMs: parseSignedReadUrlExpiresAtMs('2099-01-01T00:05:00Z'), + }; + + expect(shouldReuseSignedReadUrlCacheEntry(entry, nowMs)).toBe(true); + expect(shouldReuseSignedReadUrlCacheEntry(entry, nowMs + 269_999)).toBe( + true, + ); + expect(shouldReuseSignedReadUrlCacheEntry(entry, nowMs + 270_000)).toBe( + false, + ); + }); +}); diff --git a/packages/shared/src/utils/signedReadUrlCache.ts b/packages/shared/src/utils/signedReadUrlCache.ts new file mode 100644 index 000000000..5558dfba1 --- /dev/null +++ b/packages/shared/src/utils/signedReadUrlCache.ts @@ -0,0 +1,26 @@ +export const SIGNED_READ_URL_CACHE_SAFETY_WINDOW_MS = 30 * 1000; + +export type SignedReadUrlCacheEntry = { + signedUrl: string; + expiresAtMs: number; +}; + +export function parseSignedReadUrlExpiresAtMs(expiresAt: string) { + if (!expiresAt) { + return 0; + } + + const parsed = Date.parse(expiresAt); + return Number.isFinite(parsed) ? parsed : 0; +} + +export function shouldReuseSignedReadUrlCacheEntry( + entry: SignedReadUrlCacheEntry | undefined, + nowMs = Date.now(), +) { + if (!entry) { + return false; + } + + return entry.expiresAtMs - SIGNED_READ_URL_CACHE_SAFETY_WINDOW_MS > nowMs; +} diff --git a/src/services/assetReadUrlService.ts b/src/services/assetReadUrlService.ts index 85feb9380..bf80f2772 100644 --- a/src/services/assetReadUrlService.ts +++ b/src/services/assetReadUrlService.ts @@ -2,6 +2,11 @@ import { appendApiErrorRequestId, parseApiErrorMessage, } from '../../packages/shared/src/http'; +import { + parseSignedReadUrlExpiresAtMs, + shouldReuseSignedReadUrlCacheEntry, + type SignedReadUrlCacheEntry, +} from '../../packages/shared/src/utils/signedReadUrlCache'; import { ApiClientError, type ApiRequestOptions, @@ -45,18 +50,12 @@ export type AssetReadUrlResponse = { expiresAt?: string; }; -type CachedReadUrlEntry = { - signedUrl: string; - expiresAtMs: number; -}; - type CachedReadUrlFailureEntry = { expiresAtMs: number; }; const ASSET_READ_URL_API_PATH = '/api/assets/read-url'; const ASSET_READ_BYTES_API_PATH = '/api/assets/read-bytes'; -const DEFAULT_CACHE_SAFETY_WINDOW_MS = 30 * 1000; const DEFAULT_FAILURE_CACHE_WINDOW_MS = 60 * 1000; const ASSET_READ_URL_BACKGROUND_OPTIONS = BACKGROUND_AUTH_REQUEST_OPTIONS satisfies ApiRequestOptions; @@ -64,7 +63,7 @@ const SIGNED_READ_URL_SESSION_CACHE_PREFIX = 'genarrative.assetReadUrlCache.v1:'; const SIGNED_READ_URL_INITIAL_DISPATCH_BURST = 24; const SIGNED_READ_URL_DISPATCH_SPACING_MS = 16; -const signedReadUrlCache = new Map(); +const signedReadUrlCache = new Map(); const signedReadUrlFailureCache = new Map(); const pendingSignedReadUrlRequests = new Map>(); let signedReadUrlDispatchBurstUsed = 0; @@ -173,23 +172,6 @@ function resolveSignedReadPayload(response: AssetReadUrlResponse) { }; } -function parseExpiresAtMs(expiresAt: string) { - if (!expiresAt) { - return 0; - } - - const parsed = Date.parse(expiresAt); - return Number.isFinite(parsed) ? parsed : 0; -} - -function shouldReuseCachedReadUrl(entry: CachedReadUrlEntry | undefined) { - if (!entry) { - return false; - } - - return entry.expiresAtMs - DEFAULT_CACHE_SAFETY_WINDOW_MS > Date.now(); -} - function shouldReuseCachedReadUrlFailure( entry: CachedReadUrlFailureEntry | undefined, ) { @@ -214,7 +196,7 @@ function getSignedReadUrlSessionStorage() { function readSignedUrlSessionCache( cacheKey: string, -): CachedReadUrlEntry | undefined { +): SignedReadUrlCacheEntry | undefined { const storage = getSignedReadUrlSessionStorage(); if (!storage || !cacheKey) { return undefined; @@ -225,7 +207,7 @@ function readSignedUrlSessionCache( if (!rawValue) { return undefined; } - const parsed = JSON.parse(rawValue) as Partial; + const parsed = JSON.parse(rawValue) as Partial; if ( typeof parsed.signedUrl !== 'string' || typeof parsed.expiresAtMs !== 'number' || @@ -250,7 +232,7 @@ function readSignedUrlSessionCache( function writeSignedUrlSessionCache( cacheKey: string, - entry: CachedReadUrlEntry, + entry: SignedReadUrlCacheEntry, ) { const storage = getSignedReadUrlSessionStorage(); if (!storage || !cacheKey) { @@ -358,12 +340,12 @@ export async function getSignedAssetReadUrl( const bypassCache = options.bypassCache === true; const cached = !bypassCache && cacheKey ? signedReadUrlCache.get(cacheKey) : undefined; - if (cached && shouldReuseCachedReadUrl(cached)) { + if (cached && shouldReuseSignedReadUrlCacheEntry(cached)) { return cached.signedUrl; } const sessionCached = !bypassCache && cacheKey ? readSignedUrlSessionCache(cacheKey) : undefined; - if (sessionCached && shouldReuseCachedReadUrl(sessionCached)) { + if (sessionCached && shouldReuseSignedReadUrlCacheEntry(sessionCached)) { signedReadUrlCache.set(cacheKey, sessionCached); return sessionCached.signedUrl; } @@ -400,7 +382,7 @@ export async function getSignedAssetReadUrl( }, ); const payload = resolveSignedReadPayload(response); - const expiresAtMs = parseExpiresAtMs(payload.expiresAt); + const expiresAtMs = parseSignedReadUrlExpiresAtMs(payload.expiresAt); if (cacheKey) { signedReadUrlFailureCache.delete(cacheKey);