assets without objects
This commit is contained in:
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user