fix(markdown): retry transient image decode failures

This commit is contained in:
Mohamed Boudra
2026-10-15 16:55:15 +02:00
parent b88b018d5f
commit ad4d910aef
3 changed files with 45 additions and 11 deletions
@@ -0,0 +1,18 @@
type ImageDecoder = (
uri: string,
success: (width: number, height: number) => void,
failure: (error: unknown) => void,
) => void;
export function decodeMarkdownImage(uri: string, decode: ImageDecoder): Promise<true> {
return new Promise((resolve, reject) => {
decode(
uri,
(width, height) => {
if (width > 0 && height > 0) resolve(true);
else reject(new Error("Image has no decodable dimensions"));
},
reject,
);
});
}
+6 -11
View File
@@ -15,6 +15,7 @@ import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { AttachmentMetadata } from "@/attachments/types";
import { resolveMarkdownPreviewImageSource } from "./image-source";
import { prepareMarkdownSource } from "./source";
import { decodeMarkdownImage } from "./image-decoding";
const urls = createAssistantImageAcquisitionCache<{ uri: string; attachment: AttachmentMetadata }>({
capacity: 500,
@@ -99,11 +100,15 @@ export function useMarkdownSource(text: string, context: MarkdownImageContext) {
const decoded = useFetchQueries(
displayUrls.map((uri, index) => ({
queryKey: ["markdownImageDecode", occurrenceKey, uri ?? sources[index]],
refetchOnReconnect: true,
enabled: uri !== undefined,
immutableWhen: () => true,
dataShape: "value",
gcTime: 0,
queryFn: () => (uri ? canDecodeImage(uri) : Promise.resolve(false)),
queryFn: () => {
if (!uri) throw new Error("No image URL to decode");
return decodeMarkdownImage(uri, Image.getSize);
},
})),
);
const unavailableImages = new Set(
@@ -118,13 +123,3 @@ export function useMarkdownSource(text: string, context: MarkdownImageContext) {
imageSources: Object.fromEntries([...resolved].map(([source, url]) => [url, source])),
};
}
function canDecodeImage(uri: string): Promise<boolean> {
return new Promise((resolve) => {
Image.getSize(
uri,
(width, height) => resolve(width > 0 && height > 0),
() => resolve(false),
);
});
}
@@ -1,3 +1,5 @@
import { QueryClient } from "@tanstack/react-query";
import { decodeMarkdownImage } from "./image-decoding";
import { createElement } from "react";
import { MarkdownRendererFrame } from "./frame";
import { describe, expect, it } from "vitest";
@@ -348,3 +350,22 @@ it("preserves literal Markdown punctuation in unsupported and failed image label
),
).toBe(label);
});
it("retries transient image decoding failures instead of caching an unavailable image", async () => {
const client = new QueryClient();
let attempts = 0;
const decoded = await client.fetchQuery({
queryKey: ["markdownImageDecode", "transient.png"],
retry: 1,
retryDelay: 0,
queryFn: () =>
decodeMarkdownImage("transient.png", (_uri, success, failure) => {
attempts += 1;
if (attempts === 1) failure(new Error("temporarily unavailable"));
else success(20, 20);
}),
});
expect(decoded).toBe(true);
expect(attempts).toBe(2);
client.clear();
});