assets without objects

This commit is contained in:
2025-11-20 02:32:11 +01:00
parent 0eb4c0a294
commit 78e6d3e431
6 changed files with 59 additions and 139 deletions
+4 -21
View File
@@ -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
);
};
+46 -105
View File
@@ -17,6 +17,7 @@ export interface AssetLike {
asset_type?: string;
cardinality?: number | null;
url?: string | null;
expires_at?: number | null;
metadata?: Record<string, unknown> | null;
expiresAt?: number | null;
assets?: Record<string, AssetLike> | 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<unknown>;
export type GetAsset = (document: DocumentLike, assetType: string) => Nullable<AssetLike>;
@@ -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<number, AssetObject>();
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<string> => {
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<AssetLike>,
{ force = false, start = null, limit = null }: { force?: boolean; start?: number | null; limit?: number | null } = {},
{ force = false }: { force?: boolean } = {},
): Promise<Nullable<AssetLike>> {
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<string, number> = {};
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<AssetLike | null> = 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,
};
+1 -2
View File
@@ -12,10 +12,9 @@ interface DocumentLike {
interface AssetLike {
id?: Identifier;
cardinality?: number;
url?: string | null;
expires_at?: number | null;
metadata?: Record<string, unknown> | null;
objects?: Array<Record<string, unknown>>;
[key: string]: unknown;
}
@@ -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) {
+4 -2
View File
@@ -11,12 +11,14 @@ type DocumentLike = {
type AssetObject = {
url?: string | null;
metadata?: Record<string, unknown> | null;
expires_at?: number | null;
[key: string]: unknown;
};
type AssetLike = {
id?: Identifier;
url?: string | null;
expires_at?: number | null;
metadata?: Record<string, unknown> | 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<unknown>;
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) {
+3 -6
View File
@@ -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<DocumentLike |
export type EnsureAssetUrl = (
id: string | number,
asset: AssetLike,
options?: { start?: number; limit?: number; force?: boolean },
options?: { force?: boolean },
) => Promise<AssetLike | null>;
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
);
}