Fix canvas transfer timeouts and move images out of snapshots
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import type { CanvasDocument } from "./canvas.ts";
|
||||
|
||||
type Request = <T>(path: string, options?: RequestInit) => Promise<T>;
|
||||
export type CanvasAsset = { id: string; url: string; width: number; height: number; bytes: number; mime_type: string };
|
||||
export const ASSET_LIMIT = 20 * 1024 * 1024;
|
||||
export function inlineImageCount(document: CanvasDocument) {
|
||||
return document.nodes.filter((node) => ["image", "plain-image"].includes(node.data.kind) && node.data.src?.startsWith("data:image/")).length;
|
||||
}
|
||||
export async function uploadCanvasImage(request: Request, workspace: string, file: Blob, name: string, signal?: AbortSignal): Promise<CanvasAsset> {
|
||||
if (!file.size || file.size > ASSET_LIMIT) throw new Error("asset_too_large");
|
||||
const form = new FormData(); form.append("image", file, name);
|
||||
const key = crypto.randomUUID();
|
||||
const send = () => request<CanvasAsset>(`workspaces/${encodeURIComponent(workspace)}/assets`, { method: "POST", headers: { "Idempotency-Key": key }, body: form, signal });
|
||||
let asset: CanvasAsset;
|
||||
try { asset = await send(); } catch (error) {
|
||||
if (signal?.aborted || !(error instanceof TypeError || typeof error === "object" && error && "status" in error && Number(error.status) >= 500)) throw error;
|
||||
asset = await send(); // The same bytes/key retry a possibly committed upload.
|
||||
}
|
||||
if (!asset || typeof asset.url !== "string") throw new Error("invalid_asset_response");
|
||||
let url: URL;
|
||||
try { url = new URL(asset.url); } catch { throw new Error("invalid_asset_response"); }
|
||||
if (!/^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i.test(asset.id) || url.origin !== "https://api.foundry.entroia.com" || url.pathname !== `/projects/${process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap"}/workspaces/${workspace}/assets/${asset.id}` || url.search || !Number.isFinite(asset.width) || asset.width < 1 || !Number.isFinite(asset.height) || asset.height < 1) throw new Error("invalid_asset_response");
|
||||
return asset;
|
||||
}
|
||||
export function inlineImageBlob(source: string) {
|
||||
const match = /^data:image\/(png|jpeg|webp|gif);base64,([A-Za-z0-9+/]+={0,2})$/.exec(source);
|
||||
if (!match || match[2].length > Math.ceil(ASSET_LIMIT / 3) * 4) throw new Error("invalid_asset");
|
||||
const bytes = Uint8Array.from(atob(match[2]), (char) => char.charCodeAt(0));
|
||||
return { file: new Blob([bytes], { type: "image/" + match[1] }), extension: match[1] };
|
||||
}
|
||||
/** Explicit one-time action: preserve image bytes and all node/edge identities. */
|
||||
export async function prepareManagedImages(document: CanvasDocument, request: Request, workspace: string, signal?: AbortSignal) {
|
||||
const assets = new Map<string, CanvasAsset>();
|
||||
const nodes = [];
|
||||
for (const node of document.nodes) {
|
||||
const source = node.data.src;
|
||||
if (!["image", "plain-image"].includes(node.data.kind) || !source?.startsWith("data:image/")) { nodes.push(node); continue; }
|
||||
let asset = assets.get(source);
|
||||
if (!asset) { const { file, extension } = inlineImageBlob(source); asset = await uploadCanvasImage(request, workspace, file, `image-${node.id}.${extension}`, signal); assets.set(source, asset); }
|
||||
nodes.push({ ...node, data: { ...node.data, src: asset.url, ...(document.schema_version === 2 ? { assetId: asset.id } : {}) } });
|
||||
}
|
||||
return { ...document, nodes };
|
||||
}
|
||||
Reference in New Issue
Block a user