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
+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,
};