search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

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

fix: blento URLs, parallel search prefetch, tpuf keepalive

- skip blento.app base_paths in URL construction (blento is a card
portal, not a document server) — affected docs fall through to
leaflet.pub/p/{did}/{rkey} or platform-specific fallbacks
- prefetch search API call in parallel with atlas.json when ?q= is
present, eliminating stacked latency on shared links
- add 3-minute tpuf keepalive ping to prevent cold start penalty
(~600-900ms extra on first query after inactivity)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+107 -66
+1
backend/src/main.zig
··· 88 88 89 89 // init vector store (reads TURBOPUFFER_API_KEY from env) 90 90 tpuf.init(); 91 + tpuf.startKeepalive(allocator); 91 92 92 93 // start reconciler (verifies documents still exist at source PDS) 93 94 ingest.reconciler.start(allocator);
+4 -2
backend/src/server/search.zig
··· 113 113 if (std.mem.eql(u8, doc_type, "publication") and base_path.len > 0) { 114 114 return std.fmt.allocPrint(alloc, "https://{s}", .{base_path}) catch ""; 115 115 } 116 + // skip non-document-serving hosts (blento is a card portal, not a document platform) 117 + const usable_base = base_path.len > 0 and !std.mem.startsWith(u8, base_path, "blento.app"); 116 118 // leaflet + basePath + rkey → https://{basePath}/{rkey} 117 - if (std.mem.eql(u8, platform, "leaflet") and base_path.len > 0 and rkey.len > 0) { 119 + if (std.mem.eql(u8, platform, "leaflet") and usable_base and rkey.len > 0) { 118 120 return std.fmt.allocPrint(alloc, "https://{s}/{s}", .{ base_path, rkey }) catch ""; 119 121 } 120 122 // basePath + path → https://{basePath}[/]{path} 121 - if (base_path.len > 0 and path.len > 0) { 123 + if (usable_base and path.len > 0) { 122 124 const sep: []const u8 = if (path[0] == '/') "" else "/"; 123 125 return std.fmt.allocPrint(alloc, "https://{s}{s}{s}", .{ base_path, sep, path }) catch ""; 124 126 }
+30
backend/src/tpuf.zig
··· 586 586 587 587 return try response_body.toOwnedSlice(); 588 588 } 589 + 590 + // --- keepalive --- 591 + 592 + const KEEPALIVE_INTERVAL_NS: u64 = 3 * 60 * std.time.ns_per_s; // 3 minutes 593 + 594 + /// Start a background thread that pings turbopuffer periodically to prevent cold starts. 595 + /// Cold starts add ~600-900ms to the first query after inactivity. 596 + pub fn startKeepalive(allocator: Allocator) void { 597 + if (api_key == null) return; 598 + const thread = std.Thread.spawn(.{}, keepaliveLoop, .{allocator}) catch |err| { 599 + logfire.warn("tpuf: failed to start keepalive thread: {}", .{err}); 600 + return; 601 + }; 602 + thread.detach(); 603 + logfire.info("tpuf: keepalive started (interval=3m)", .{}); 604 + } 605 + 606 + fn keepaliveLoop(allocator: Allocator) void { 607 + // minimal query body: rank by ID, top_k=1, no attributes 608 + const ping_body = "{\"rank_by\":[\"id\",\"asc\"],\"top_k\":1,\"include_attributes\":[]}"; 609 + while (true) { 610 + std.Thread.sleep(KEEPALIVE_INTERVAL_NS); 611 + const key = api_key orelse return; 612 + const response = doRequest(allocator, key, query_url, ping_body) catch |err| { 613 + logfire.debug("tpuf: keepalive ping failed: {}", .{err}); 614 + continue; 615 + }; 616 + allocator.free(response); 617 + } 618 + }
+72 -64
site/atlas.js
··· 715 715 if (!m) return null; 716 716 var did = m[1], collection = m[2], rkey = m[3]; 717 717 if (platform === 'whitewind' || collection.startsWith('com.whtwnd.')) return 'https://whtwnd.com/' + did + '/' + rkey; 718 + // skip non-document-serving hosts (blento is a card portal, not a document platform) 719 + var usableBase = basePath && !basePath.startsWith('blento.app'); 718 720 // leaflet uses rkey directly 719 - if (platform === 'leaflet' && basePath) return 'https://' + basePath + '/' + rkey; 721 + if (platform === 'leaflet' && usableBase) return 'https://' + basePath + '/' + rkey; 720 722 // leaflet without basePath 721 723 if (platform === 'leaflet') return 'https://leaflet.pub/p/' + did + '/' + rkey; 722 724 // other platforms (pckt, offprint, etc.) use path slug when available 723 - if (basePath && path) { 725 + if (usableBase && path) { 724 726 var sep = path.charAt(0) === '/' ? '' : '/'; 725 727 return 'https://' + basePath + sep + path; 726 728 } 727 - if (basePath) return 'https://' + basePath + '/' + rkey; 729 + if (usableBase) return 'https://' + basePath + '/' + rkey; 728 730 // universal fallback — AT Protocol record viewer 729 731 return 'https://pdsls.dev/at/' + did + '/' + collection + '/' + rkey; 730 732 } ··· 801 803 d.clusters.fine.length + ' clusters'; 802 804 document.getElementById('loading').classList.add('hidden'); 803 805 view.dirty = true; 804 - // trigger search from URL ?q= param 805 - var urlQ = new URLSearchParams(window.location.search).get('q'); 806 - if (urlQ) { 807 - searchInput.value = urlQ; 808 - doSearch(urlQ, true); 806 + // apply prefetched search results (fired in parallel with atlas.json) 807 + if (pendingSearchResults) { 808 + pendingSearchResults.then(function(resp) { 809 + pendingSearchResults = null; 810 + if (resp) applySearchResults(resp, searchInput.value); 811 + }); 809 812 } 810 813 }) 811 814 .catch(function(err) { ··· 816 819 817 820 window.addEventListener('resize', resizeCanvas); 818 821 resizeCanvas(); 822 + // prefetch search results in parallel with atlas.json when ?q= is present 823 + var urlQ = new URLSearchParams(window.location.search).get('q'); 824 + if (urlQ) { 825 + searchInput.value = urlQ; 826 + pendingSearchResults = fetch(API_URL + '/search?mode=semantic&limit=20&format=v2&q=' + encodeURIComponent(urlQ)) 827 + .then(function(r) { return r.ok ? r.json() : null; }) 828 + .catch(function() { return null; }); 829 + } 819 830 loadData(); 820 831 loop(); 821 832 ··· 824 835 var searchInput = document.getElementById('search-input'); 825 836 var searchForm = document.getElementById('search-form'); 826 837 var searchStatusEl = null; 838 + var pendingSearchResults = null; // promise for prefetched search results 827 839 828 840 function setSearchStatus(msg) { 829 841 if (!searchStatusEl) { ··· 847 859 view.dirty = true; 848 860 } 849 861 862 + function applySearchResults(resp, query) { 863 + searchQuery = query; 864 + var results = (resp && resp.results) || []; 865 + if (results.length === 0) { 866 + setSearchStatus('no results'); 867 + searchMatches = null; 868 + searchCenter = null; 869 + view.dirty = true; 870 + return; 871 + } 872 + 873 + // match result URIs to atlas points 874 + var matches = new Set(); 875 + var weightedX = 0, weightedY = 0, totalWeight = 0; 876 + for (var i = 0; i < results.length; i++) { 877 + var uri = results[i].uri; 878 + if (uriToIndex && uriToIndex.has(uri)) { 879 + var idx = uriToIndex.get(uri); 880 + matches.add(idx); 881 + var w = results.length - i; 882 + weightedX += pointsX[idx] * w; 883 + weightedY += pointsY[idx] * w; 884 + totalWeight += w; 885 + } 886 + } 887 + 888 + if (matches.size === 0) { 889 + setSearchStatus(results.length + ' results, 0 on map'); 890 + searchMatches = null; 891 + searchCenter = null; 892 + view.dirty = true; 893 + return; 894 + } 895 + 896 + searchMatches = matches; 897 + searchCenter = { x: weightedX / totalWeight, y: weightedY / totalWeight }; 898 + setSearchStatus(matches.size + ' of ' + results.length + ' on map'); 899 + 900 + var maxDist = 0; 901 + matches.forEach(function(idx) { 902 + var dx = pointsX[idx] - searchCenter.x; 903 + var dy = pointsY[idx] - searchCenter.y; 904 + var d = Math.sqrt(dx * dx + dy * dy); 905 + if (d > maxDist) maxDist = d; 906 + }); 907 + 908 + var targetZoom = maxDist > 0 ? Math.min(view.maxZoom, 0.3 / maxDist) : 6; 909 + targetZoom = Math.max(4, Math.min(15, targetZoom)); 910 + animateTo(searchCenter.x, searchCenter.y, targetZoom); 911 + } 912 + 850 913 function doSearch(query, skipPush) { 851 914 if (!query || !data || !uriToIndex) return; 852 - searchQuery = query; 853 915 setSearchStatus('searching...'); 854 916 if (!skipPush) { 855 917 var url = new URL(window.location); ··· 862 924 if (!r.ok) throw new Error('search failed: ' + r.status); 863 925 return r.json(); 864 926 }) 865 - .then(function(resp) { 866 - var results = resp.results || []; 867 - if (results.length === 0) { 868 - setSearchStatus('no results'); 869 - searchMatches = null; 870 - searchCenter = null; 871 - view.dirty = true; 872 - return; 873 - } 874 - 875 - // match result URIs to atlas points 876 - var matches = new Set(); 877 - var weightedX = 0, weightedY = 0, totalWeight = 0; 878 - for (var i = 0; i < results.length; i++) { 879 - var uri = results[i].uri; 880 - if (uriToIndex.has(uri)) { 881 - var idx = uriToIndex.get(uri); 882 - matches.add(idx); 883 - // weight by rank (higher rank = more weight) 884 - var w = results.length - i; 885 - weightedX += pointsX[idx] * w; 886 - weightedY += pointsY[idx] * w; 887 - totalWeight += w; 888 - } 889 - } 890 - 891 - if (matches.size === 0) { 892 - setSearchStatus(results.length + ' results, 0 on map'); 893 - searchMatches = null; 894 - searchCenter = null; 895 - view.dirty = true; 896 - return; 897 - } 898 - 899 - searchMatches = matches; 900 - searchCenter = { x: weightedX / totalWeight, y: weightedY / totalWeight }; 901 - setSearchStatus(matches.size + ' of ' + results.length + ' on map'); 902 - 903 - // compute spread to determine zoom level 904 - var maxDist = 0; 905 - matches.forEach(function(idx) { 906 - var dx = pointsX[idx] - searchCenter.x; 907 - var dy = pointsY[idx] - searchCenter.y; 908 - var d = Math.sqrt(dx * dx + dy * dy); 909 - if (d > maxDist) maxDist = d; 910 - }); 911 - 912 - // zoom to fit the spread with some padding 913 - // at zoom=1, visible radius in data coords is ~1.0 (since range is [-1,1]) 914 - // we want maxDist to fit in ~30% of the viewport 915 - var targetZoom = maxDist > 0 ? Math.min(view.maxZoom, 0.3 / maxDist) : 6; 916 - targetZoom = Math.max(4, Math.min(15, targetZoom)); 917 - 918 - animateTo(searchCenter.x, searchCenter.y, targetZoom); 919 - }) 927 + .then(function(resp) { applySearchResults(resp, query); }) 920 928 .catch(function(err) { 921 929 setSearchStatus('error'); 922 930 console.error(err);