Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

keeps: warm public IPFS gateways after upload + after resync tx

Uploads in keep-prepare-background and keep-update now also fire-and-forget
a Range: bytes=0-0 GET to ipfs.io/gateway.ipfs.io/dweb.link/nftstorage.link
per CID, triggering those gateways to DHT-fetch from our Kubo node and cache.
Once cached there, objkt's indexer and the assets.objkt.media thumbnail
pipeline have multiple well-peered providers to pull from instead of timing
out against a single private node.

Adds /api/warm-gateways endpoint (POST {cids:[...]}) and a post-tx client
step in syncRebakeMetadataOnChain that calls it for the final artifact/
thumbnail/metadata CIDs, surfacing progress in the track log so the user
sees "Warming public IPFS gateways for N CIDs..." → "Gateways warmed: ..."
as visible steps in the resync confirmation panel.

Large artifact HTMLs (~1MB+) may still 504 at the public gateways'
internal timeouts — Filebase or a proper pinning service remains the
durable fix for those. Small files (metadata JSON, animated WebP thumbs)
land reliably.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+150 -3
+25
system/netlify/functions/keep-prepare-background.mjs
··· 406 406 .catch(() => {}); // Best-effort, don't block pipeline 407 407 } 408 408 409 + // Warm public IPFS gateways so they DHT-fetch the CID from us and cache it. 410 + // Once cached on e.g. ipfs.io, there are multiple providers for downstream 411 + // indexers (objkt, tzkt) to find, which dramatically cuts the lag between 412 + // edit_metadata and rebaked thumbnails appearing in their UIs. Fire-and-forget; 413 + // Range header keeps our egress tiny while the gateway fetches the full CID. 414 + const PUBLIC_GATEWAYS = [ 415 + "https://ipfs.io", 416 + "https://gateway.ipfs.io", 417 + "https://dweb.link", 418 + "https://nftstorage.link", 419 + ]; 420 + function warmPublicGateways(hash) { 421 + for (const gw of PUBLIC_GATEWAYS) { 422 + fetch(`${gw}/ipfs/${hash}`, { 423 + method: "GET", 424 + headers: { Range: "bytes=0-0" }, 425 + redirect: "follow", 426 + signal: AbortSignal.timeout(20000), 427 + }).catch(() => {}); // Best-effort, don't block pipeline 428 + } 429 + console.log(`🔥 Warming ${hash.slice(0, 12)}... on ${PUBLIC_GATEWAYS.length} public gateways`); 430 + } 431 + 409 432 async function uploadToIPFS(content, filename, mimeType, timeoutMs = 90000) { 410 433 const formData = new FormData(); 411 434 formData.append("file", new Blob([content], { type: mimeType }), filename); ··· 422 445 const result = await res.json(); 423 446 seedToSecondaryNode(result.Hash); 424 447 pinToPublicService(result.Hash, filename); 448 + warmPublicGateways(result.Hash); 425 449 return formatIpfsUri(result.Hash); 426 450 } catch (err) { 427 451 clearTimeout(timeout); ··· 447 471 const result = await res.json(); 448 472 seedToSecondaryNode(result.Hash); 449 473 pinToPublicService(result.Hash, name); 474 + warmPublicGateways(result.Hash); 450 475 return formatIpfsUri(result.Hash); 451 476 } catch (err) { 452 477 clearTimeout(timeout);
+22
system/netlify/functions/keep-update.mjs
··· 77 77 .catch(() => {}); // Best-effort, don't block pipeline 78 78 } 79 79 80 + // Warm public IPFS gateways so they DHT-fetch and cache the CID, giving 81 + // downstream indexers (objkt, tzkt) multiple providers to find. Keeps objkt 82 + // from seeing stale metadata when our self-hosted node is the sole provider. 83 + const PUBLIC_GATEWAYS = [ 84 + "https://ipfs.io", 85 + "https://gateway.ipfs.io", 86 + "https://dweb.link", 87 + "https://nftstorage.link", 88 + ]; 89 + function warmPublicGateways(hash) { 90 + for (const gw of PUBLIC_GATEWAYS) { 91 + fetch(`${gw}/ipfs/${hash}`, { 92 + method: "GET", 93 + headers: { Range: "bytes=0-0" }, 94 + redirect: "follow", 95 + signal: AbortSignal.timeout(20000), 96 + }).catch(() => {}); // Best-effort, don't block pipeline 97 + } 98 + console.log(`🔥 KEEP-UPDATE: warming ${hash.slice(0, 12)}... on ${PUBLIC_GATEWAYS.length} public gateways`); 99 + } 100 + 80 101 async function uploadJsonToIPFS(data, name, timeoutMs = 30000) { 81 102 const content = JSON.stringify(data); 82 103 const formData = new FormData(); ··· 94 115 const result = await res.json(); 95 116 seedToSecondaryNode(result.Hash); 96 117 pinToPublicService(result.Hash, name); 118 + warmPublicGateways(result.Hash); 97 119 return formatIpfsUri(result.Hash); 98 120 } catch (err) { 99 121 clearTimeout(timeout);
+69
system/netlify/functions/warm-gateways.mjs
··· 1 + // warm-gateways.mjs — Best-effort gateway warming for a set of IPFS CIDs. 2 + // 3 + // POST /api/warm-gateways { cids: ["Qm...", ...] } 4 + // 5 + // Triggers public IPFS gateways to fetch each CID from the DHT (finding us as 6 + // a provider) and cache it. Once cached there, downstream indexers like objkt 7 + // have multiple places to fetch from, cutting the lag after on-chain metadata 8 + // updates. Fire-and-forget per request — returns 200 immediately. 9 + // 10 + // The server-side upload pipeline already warms gateways at bake time; this 11 + // endpoint exists so the post-tx confirmation UI can re-warm right before the 12 + // on-chain update is visible to indexers, shown as a visible track step. 13 + 14 + const PUBLIC_GATEWAYS = [ 15 + "https://ipfs.io", 16 + "https://gateway.ipfs.io", 17 + "https://dweb.link", 18 + "https://nftstorage.link", 19 + ]; 20 + 21 + const CORS_HEADERS = { 22 + "Access-Control-Allow-Origin": "*", 23 + "Access-Control-Allow-Headers": "Content-Type, Authorization", 24 + "Content-Type": "application/json", 25 + }; 26 + 27 + function isValidCid(v) { 28 + return typeof v === "string" && /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|ba[a-z0-9]{56,})$/.test(v.trim()); 29 + } 30 + 31 + export const handler = async (event) => { 32 + if (event.httpMethod === "OPTIONS") { 33 + return { statusCode: 200, headers: CORS_HEADERS, body: "" }; 34 + } 35 + if (event.httpMethod !== "POST") { 36 + return { statusCode: 405, headers: CORS_HEADERS, body: JSON.stringify({ error: "Method not allowed" }) }; 37 + } 38 + 39 + let body; 40 + try { body = JSON.parse(event.body || "{}"); } 41 + catch { return { statusCode: 400, headers: CORS_HEADERS, body: JSON.stringify({ error: "Invalid JSON body" }) }; } 42 + 43 + const cids = Array.isArray(body.cids) ? body.cids.filter(isValidCid) : []; 44 + if (!cids.length) { 45 + return { statusCode: 400, headers: CORS_HEADERS, body: JSON.stringify({ error: "cids array required" }) }; 46 + } 47 + 48 + for (const cid of cids) { 49 + for (const gw of PUBLIC_GATEWAYS) { 50 + fetch(`${gw}/ipfs/${cid}`, { 51 + method: "GET", 52 + headers: { Range: "bytes=0-0" }, 53 + redirect: "follow", 54 + signal: AbortSignal.timeout(20000), 55 + }).catch(() => {}); 56 + } 57 + console.log(`🔥 WARM-GATEWAYS: ${cid.slice(0, 12)}... fanned out to ${PUBLIC_GATEWAYS.length} gateways`); 58 + } 59 + 60 + return { 61 + statusCode: 200, 62 + headers: CORS_HEADERS, 63 + body: JSON.stringify({ 64 + ok: true, 65 + cidCount: cids.length, 66 + gateways: PUBLIC_GATEWAYS.map((u) => u.replace(/^https:\/\//, "")), 67 + }), 68 + }; 69 + };
+34 -3
system/public/kidlisp.com/keeps.html
··· 6748 6748 throw new Error(confirmData?.error || `Metadata update confirm failed (${confirmResponse.status})`); 6749 6749 } 6750 6750 6751 + // Warm public IPFS gateways for the final URIs so objkt's indexer has 6752 + // well-peered sources the moment it re-reads the on-chain pointer. 6753 + // Fire-and-forget but surfaced in the track log so the user sees it. 6754 + const finalArtifact = confirmData?.artifactUri || confirmPayload.artifactUri; 6755 + const finalThumb = confirmData?.thumbnailUri || confirmPayload.thumbnailUri; 6756 + const finalMeta = confirmData?.metadataUri || confirmPayload.metadataUri; 6757 + const cidsToWarm = [finalArtifact, finalThumb, finalMeta] 6758 + .filter(Boolean) 6759 + .map((u) => (u.startsWith('ipfs://') ? u.slice(7) : u)) 6760 + .filter((cid) => /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|ba[a-z0-9]{56,})$/.test(cid)); 6761 + if (cidsToWarm.length) { 6762 + addTrackEntry(`Warming public IPFS gateways for ${cidsToWarm.length} CID${cidsToWarm.length === 1 ? '' : 's'}...`, 'active'); 6763 + try { 6764 + const warmRes = await fetch(`${API_BASE}/api/warm-gateways`, { 6765 + method: 'POST', 6766 + headers: { 'Content-Type': 'application/json' }, 6767 + body: JSON.stringify({ cids: cidsToWarm }), 6768 + }); 6769 + const warmData = await warmRes.json().catch(() => null); 6770 + if (warmRes.ok && warmData?.ok) { 6771 + const gws = (warmData.gateways || []).join(', '); 6772 + addTrackEntry(`Gateways warmed: ${gws}`, 'done'); 6773 + } else { 6774 + addTrackEntry('Gateway warming skipped (non-critical)', 'done'); 6775 + } 6776 + } catch (warmErr) { 6777 + addTrackEntry('Gateway warming failed (non-critical)', 'done'); 6778 + console.warn('warm-gateways failed:', warmErr); 6779 + } 6780 + } 6781 + 6751 6782 return { 6752 6783 txHash, 6753 6784 tokenId: normalizedTokenId, ··· 6755 6786 contractAddress: confirmPayload.contractAddress, 6756 6787 contractProfile: confirmData?.contractProfile || confirmPayload.contractProfile || null, 6757 6788 contractVersion: confirmData?.contractVersion || confirmPayload.contractVersion || null, 6758 - artifactUri: confirmData?.artifactUri || confirmPayload.artifactUri, 6759 - thumbnailUri: confirmData?.thumbnailUri || confirmPayload.thumbnailUri, 6760 - metadataUri: confirmData?.metadataUri || confirmPayload.metadataUri, 6789 + artifactUri: finalArtifact, 6790 + thumbnailUri: finalThumb, 6791 + metadataUri: finalMeta, 6761 6792 }; 6762 6793 } 6763 6794