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