diff --git a/frontend/src/app/appLayoutUtils.ts b/frontend/src/app/appLayoutUtils.ts index 270ac90..784b107 100644 --- a/frontend/src/app/appLayoutUtils.ts +++ b/frontend/src/app/appLayoutUtils.ts @@ -1,4 +1,4 @@ -import { createAssetView } from '../asset_manager'; +import { createAssetView, resolveAssetExpiresAt } from '../asset_manager'; export const ASSET_PRESIGN_TTL_MS = 240 * 1000; // backend issues 5 min tokens; refresh slightly early export const DEFAULT_FOLDER_NAME = 'Documents'; @@ -45,34 +45,17 @@ const isAssetEquivalent = (lhs, rhs) => { const rhsView = createAssetView(rhs); const lhsPrimaryMetadata = lhsView.getPrimaryMetadata() || lhs?.metadata; const rhsPrimaryMetadata = rhsView.getPrimaryMetadata() || rhs?.metadata; - const lhsCardinality = lhsView.getCardinality() || lhs?.cardinality || null; - const rhsCardinality = rhsView.getCardinality() || rhs?.cardinality || null; - const lhsObjects = lhsView.getObjects(); - const rhsObjects = rhsView.getObjects(); - const objectsComparable = - lhsObjects.length === rhsObjects.length - && lhsObjects.every((entry, index) => { - const other = rhsObjects[index]; - if (!other) return false; - if (entry.ordinal !== other.ordinal) return false; - if (entry.url && other.url && entry.url === other.url) { - return true; - } - if (!entry.url && !other.url) { - return JSON.stringify(entry.metadata || null) === JSON.stringify(other.metadata || null); - } - return entry.url === other.url; - }); + const lhsExpiresAt = resolveAssetExpiresAt(lhs); + const rhsExpiresAt = resolveAssetExpiresAt(rhs); return ( lhs.id === rhs.id && lhs.url === rhs.url + && lhsExpiresAt === rhsExpiresAt && lhsPrimaryMetadata?.width === rhsPrimaryMetadata?.width && lhsPrimaryMetadata?.height === rhsPrimaryMetadata?.height && lhs.mime_type === rhs.mime_type && lhs.asset_type === rhs.asset_type && lhs.updated_at === rhs.updated_at - && lhsCardinality === rhsCardinality - && objectsComparable ); }; diff --git a/frontend/src/asset_manager.ts b/frontend/src/asset_manager.ts index 6b5a5e6..deb6161 100644 --- a/frontend/src/asset_manager.ts +++ b/frontend/src/asset_manager.ts @@ -17,6 +17,7 @@ export interface AssetLike { asset_type?: string; cardinality?: number | null; url?: string | null; + expires_at?: number | null; metadata?: Record | null; expiresAt?: number | null; assets?: Record | AssetLike[] | null; @@ -36,10 +37,24 @@ export interface DocumentLike { [key: string]: unknown; } +export const resolveAssetExpiresAt = ( + asset?: { expiresAt?: number | null; expires_at?: number | null } | null, +): number | null => { + const camel = Number(asset?.expiresAt); + if (Number.isFinite(camel)) { + return camel; + } + const snake = Number(asset?.expires_at); + if (Number.isFinite(snake)) { + return snake; + } + return null; +}; + export type EnsureAssetUrl = ( documentId: Identifier, asset: AssetLike, - options?: { start?: number | null; limit?: number | null; force?: boolean; [key: string]: unknown }, + options?: { force?: boolean; [key: string]: unknown }, ) => Promise; export type GetAsset = (document: DocumentLike, assetType: string) => Nullable; @@ -76,31 +91,6 @@ const normalizeAssetObjects = (objects?: AssetObject[] | null): AssetObject[] => .sort((a, b) => a.ordinal - b.ordinal); }; -const mergeAssetObjects = ( - existingObjects?: AssetObject[] | null, - incomingObjects?: AssetObject[] | null, -): AssetObject[] => { - const merged = new Map(); - - normalizeAssetObjects(existingObjects).forEach((entry) => { - if (Number.isFinite(entry.ordinal)) { - merged.set(entry.ordinal as number, { ...entry }); - } - }); - - normalizeAssetObjects(incomingObjects).forEach((entry) => { - if (Number.isFinite(entry.ordinal)) { - const ordinal = entry.ordinal as number; - const current = merged.get(ordinal) || {}; - merged.set(ordinal, { ...current, ...entry }); - } - }); - - return Array.from(merged.entries()) - .sort((a, b) => a[0] - b[0]) - .map(([, value]) => value); -}; - export class AssetView { asset: AssetLike | null; private _objectsRef: AssetObject[] | null; @@ -117,17 +107,16 @@ export class AssetView { return 0; } - const reported = Number(this.asset.cardinality); - if (Number.isFinite(reported) && reported > 0) { - return reported; - } - const objectsCount = this.getObjects().length; if (objectsCount > 0) { return objectsCount; } - return this.asset.metadata ? 1 : 0; + if (this.asset.url || this.asset.metadata) { + return 1; + } + + return 0; } getObjects(): AssetObject[] { @@ -156,7 +145,7 @@ export class AssetView { ordinal: 1, url: this.asset.url || null, metadata: this.asset.metadata || null, - expires_at: this.asset.expiresAt ?? null, + expires_at: resolveAssetExpiresAt(this.asset), }; } } @@ -190,12 +179,10 @@ export const resolveDocumentAssetUrl = ( ensureAssetUrl, getAsset, ensureOptions, - objectOrdinal = 1, }: { ensureAssetUrl?: EnsureAssetUrl; getAsset?: GetAsset; - ensureOptions?: { start?: number; limit?: number; [key: string]: unknown }; - objectOrdinal?: number; + ensureOptions?: { force?: boolean; [key: string]: unknown }; } = {}, ): Nullable => { if (!doc || !type) { @@ -206,31 +193,21 @@ export const resolveDocumentAssetUrl = ( return null; } const view = createAssetView(asset); - const object = view.getObject(objectOrdinal); - const url = object?.url || (objectOrdinal === 1 ? view.getPrimaryUrl() : null); + const object = view.getPrimaryObject(); + const url = object?.url || view.getPrimaryUrl(); const expiresAt = Number.isFinite(object?.expires_at) ? Number(object?.expires_at) - : objectOrdinal === 1 && Number.isFinite(asset.expiresAt) - ? Number(asset.expiresAt) - : null; + : resolveAssetExpiresAt(asset); const now = Date.now(); if (url && (!expiresAt || expiresAt > now)) { return url; } if (doc.id && asset.id && ensureAssetUrl) { const force = Boolean(url && expiresAt && expiresAt <= now); - const options: { force: boolean; start?: number | null; limit?: number | null; [key: string]: unknown } = { + const options: { force: boolean; [key: string]: unknown } = { force, - start: objectOrdinal, - limit: 1, ...(ensureOptions || {}), }; - if (!options.start) { - options.start = objectOrdinal; - } - if (!options.limit) { - options.limit = 1; - } ensureAssetUrl(doc.id, asset, options).catch(() => {}); } return null; @@ -262,46 +239,34 @@ class AssetManager { ensureAsset( documentId?: Identifier | null, asset?: Nullable, - { force = false, start = null, limit = null }: { force?: boolean; start?: number | null; limit?: number | null } = {}, + { force = false }: { force?: boolean } = {}, ): Promise> { if (!documentId || !asset?.id) { return Promise.resolve(asset ?? null); } - const requestedStart = Number.isInteger(start) && start > 0 ? start : 1; - const requestedLimit = Number.isInteger(limit) && limit > 0 ? limit : 1; - const requestedEnd = requestedStart + requestedLimit - 1; - const baseAsset = this.assetCache.get(asset.id) || asset; const view = createAssetView(baseAsset); - const assetExpiresAt = Number.isFinite(baseAsset.expiresAt) ? Number(baseAsset.expiresAt) : null; + const assetExpiresAt = resolveAssetExpiresAt(baseAsset); const now = Date.now(); - const isOrdinalSatisfied = (ordinal) => { - const object = view.getObject(ordinal); - if (!object) { - return false; + const isPrimarySatisfied = () => { + const object = view.getObject(1); + if (object?.url) { + const objectExpiresAt = Number.isFinite(object.expires_at) ? Number(object.expires_at) : null; + if (!objectExpiresAt || objectExpiresAt > now) { + return true; + } } - if (!object.url) { - return false; - } - if (Number.isFinite(object.expires_at)) { - return Number(object.expires_at) > now; - } - if (ordinal === 1 && baseAsset.url && (!assetExpiresAt || assetExpiresAt > now)) { + if (baseAsset.url && (!assetExpiresAt || assetExpiresAt > now)) { return true; } - return true; + return false; }; let needsFetch = force; if (!needsFetch) { - for (let ordinal = requestedStart; ordinal <= requestedEnd; ordinal += 1) { - if (!isOrdinalSatisfied(ordinal)) { - needsFetch = true; - break; - } - } + needsFetch = !isPrimarySatisfied(); } if (!needsFetch) { @@ -309,7 +274,7 @@ class AssetManager { return Promise.resolve(baseAsset); } - const inflightKey = `${documentId}:${asset.id}:${start ?? 'd'}:${limit ?? 'd'}`; + const inflightKey = `${documentId}:${asset.id}`; if (!force && this.assetInflight.has(inflightKey)) { return this.assetInflight.get(inflightKey); } @@ -318,41 +283,17 @@ class AssetManager { return Promise.reject(new Error('AssetManager API client is not configured.')); } - const params: Record = {}; - if (Number.isInteger(start) && start > 0) { - params.start = start; - } - if (Number.isInteger(limit) && limit > 0) { - params.limit = limit; - } - - const requestConfig = Object.keys(params).length ? { params } : undefined; - const request: Promise = this.api - .get(`/assets/${asset.id}`, requestConfig) + .get(`/assets/${asset.id}`) .then(({ data }) => { - const incomingObjects = Array.isArray(data.objects) ? data.objects : []; const cachedEntry = this.assetCache.get(asset.id) || baseAsset; - const mergedObjects = mergeAssetObjects(cachedEntry?.objects, incomingObjects); - const combined = { ...cachedEntry, ...asset, ...data, objects: mergedObjects }; - const view = createAssetView(combined); - const primaryObject = view.getPrimaryObject(); - const expiresAt = Number.isFinite(primaryObject?.expires_at) - ? Number(primaryObject?.expires_at) - : Date.now() + this.assetPresignTtlMs; - const cardinality = (() => { - const reported = Number(data.cardinality ?? asset.cardinality ?? cachedEntry?.cardinality); - const objectsCount = mergedObjects.length; - if (Number.isFinite(reported) && reported > 0) { - return Math.max(reported, objectsCount) || null; - } - return objectsCount || null; - })(); - + const combined = { ...cachedEntry, ...asset, ...data }; + const expiresAt = + resolveAssetExpiresAt(data) + ?? resolveAssetExpiresAt(combined) + ?? Date.now() + this.assetPresignTtlMs; const entry = { ...combined, - cardinality, - url: view.getPrimaryUrl(), expiresAt, }; diff --git a/frontend/src/desktop/DesktopPreviewCard.tsx b/frontend/src/desktop/DesktopPreviewCard.tsx index ef70bc7..b4785ed 100644 --- a/frontend/src/desktop/DesktopPreviewCard.tsx +++ b/frontend/src/desktop/DesktopPreviewCard.tsx @@ -12,10 +12,9 @@ interface DocumentLike { interface AssetLike { id?: Identifier; - cardinality?: number; url?: string | null; + expires_at?: number | null; metadata?: Record | null; - objects?: Array>; [key: string]: unknown; } diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.ts b/frontend/src/hooks/documents/useDocumentsWorkspace.ts index 8a0637f..e81d668 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.ts +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.ts @@ -1025,7 +1025,7 @@ const useDocumentsWorkspace = ({ const ensureAssetUrl = useCallback( - async (documentId, asset, { force = false, start = null, limit = null } = {}) => { + async (documentId, asset, { force = false } = {}) => { if (!documentId || !asset?.id) { return null; } @@ -1033,8 +1033,6 @@ const useDocumentsWorkspace = ({ try { const entry = await assetManager.ensureAsset(documentId, asset, { force, - start, - limit, }); if (!entry) { diff --git a/frontend/src/hooks/useAssetNavigator.ts b/frontend/src/hooks/useAssetNavigator.ts index b9274c5..02ad293 100644 --- a/frontend/src/hooks/useAssetNavigator.ts +++ b/frontend/src/hooks/useAssetNavigator.ts @@ -11,12 +11,14 @@ type DocumentLike = { type AssetObject = { url?: string | null; metadata?: Record | null; + expires_at?: number | null; [key: string]: unknown; }; type AssetLike = { id?: Identifier; url?: string | null; + expires_at?: number | null; metadata?: Record | null; objects?: AssetObject[]; [key: string]: unknown; @@ -25,7 +27,7 @@ type AssetLike = { type EnsureAssetUrl = ( documentId: Identifier, asset: AssetLike, - options?: { start?: number; limit?: number; [key: string]: unknown }, + options?: { force?: boolean; [key: string]: unknown }, ) => Promise; type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null; @@ -89,7 +91,7 @@ export const useAssetNavigator = ({ } let cancelled = false; setIsLoading(true); - ensureAssetUrl(documentId, asset, { start: 1, limit: 1 }) + ensureAssetUrl(documentId, asset, { force: true }) .catch(() => {}) .finally(() => { if (!cancelled) { diff --git a/frontend/src/utils/ocr.ts b/frontend/src/utils/ocr.ts index 95ef5cb..9d44b06 100644 --- a/frontend/src/utils/ocr.ts +++ b/frontend/src/utils/ocr.ts @@ -6,8 +6,6 @@ import type { GetAsset as AssetManagerGetAsset, } from '../asset_manager'; -const BASE_FETCH_OPTIONS = { start: 1, limit: 1 } as const; - interface DocumentVersion extends DocumentVersionLike { download_path?: string | null; } @@ -22,7 +20,7 @@ export type EnsurePreviewData = (id: string | number) => Promise Promise; export type GetDocumentAsset = AssetManagerGetAsset; @@ -70,7 +68,7 @@ export async function resolveOcrTextUrl({ let entry: AssetLike = asset; if (ensureAssetUrl) { - const ensureOptions = { ...BASE_FETCH_OPTIONS, force: !hasUrl }; + const ensureOptions = { force: !hasUrl }; const ensured = await ensureAssetUrl(docRef.id!, asset, ensureOptions); if (ensured) { entry = ensured; @@ -87,8 +85,7 @@ export async function resolveOcrTextUrl({ resolveDocumentAssetUrl(docRef, 'ocr-text', { ensureAssetUrl, getAsset: getDocumentAsset, - ensureOptions: { ...BASE_FETCH_OPTIONS, force: true }, - objectOrdinal: 1, + ensureOptions: { force: true }, }) || null ); }