···2424 command: |
2525 export PATH="$HOME/.nix-profile/bin:$PATH"
26262727- echo "This is where a build step would go... if I had one!"
2727+ bun run scripts/fetch-service-data.mjs
28282929 - name: deploy to wisp
3030 command: |
+131-26
public/api.js
···9393// ── fetchServiceData ───────────────────────────────────────────────
9494// Fetches trails, locations, activities from the service account and returns
9595// URI→record lookup maps plus normalised arrays for public consumption.
9696-let _servicePromise = null;
9696+// Uses a build-time snapshot for instant load, then revalidates in background.
9797+let _cachedServiceData = null;
9898+let _serviceInflight = null;
9999+97100export function fetchServiceData() {
9898- if (!_servicePromise) _servicePromise = _fetchServiceData();
9999- return _servicePromise;
101101+ if (_cachedServiceData) return Promise.resolve(_cachedServiceData);
102102+ if (!_serviceInflight) _serviceInflight = _fetchServiceData();
103103+ return _serviceInflight;
100104}
101105102102-async function _fetchServiceData() {
103103- const [trailRecs, locationRecs, activityRecs] = await Promise.all([
104104- listRecords('at.mapped.trail'),
105105- listRecords('at.mapped.location'),
106106- listRecords('at.mapped.activity'),
107107- ]);
108108-109109- // Activity map: uri → { uri, rkey, name }
106106+// Builds the service data object from raw PDS record arrays.
107107+// Each record is { uri, value } as returned by com.atproto.repo.listRecords.
108108+function _buildFromRecs(trailRecs, locationRecs, activityRecs) {
110109 const activityMap = new Map();
111110 for (const { uri, value } of activityRecs) {
112111 const rkey = uri.split('/').pop();
113112 activityMap.set(uri, { uri, rkey, name: value.name });
114113 }
115114116116- // Location map: uri → { uri, rkey, name, lat, lng }
117115 const locationMap = new Map();
118116 for (const { uri, value } of locationRecs) {
119117 const rkey = uri.split('/').pop();
···125123 });
126124 }
127125128128- // Trail map: uri → { uri, rkey, name, activityType, locations, geo }
129129- // locations is an array of resolved location objects (used for the filter dropdown).
130126 const trailMap = new Map();
131127 for (const { uri, value } of trailRecs) {
132128 const rkey = uri.split('/').pop();
···152148 return { trailMap, locationMap, activityMap, trails, locations, activities };
153149}
154150151151+async function _fetchServiceData() {
152152+ // Try bundled snapshot first (generated at build time)
153153+ try {
154154+ const res = await fetch('/data/service.json');
155155+ if (res.ok) {
156156+ const { trails, locations, activities } = await res.json();
157157+ const data = _buildFromRecs(trails, locations, activities);
158158+ _cachedServiceData = data;
159159+ // Revalidate from live PDS in background
160160+ _revalidateServiceData();
161161+ return data;
162162+ }
163163+ } catch (_) {
164164+ // 404 on local dev or network error — fall through to live fetch
165165+ }
166166+167167+ // Live fetch (local dev or first-ever deploy)
168168+ return _fetchLiveServiceData();
169169+}
170170+171171+async function _fetchLiveServiceData() {
172172+ const [trailRecs, locationRecs, activityRecs] = await Promise.all([
173173+ listRecords('at.mapped.trail'),
174174+ listRecords('at.mapped.location'),
175175+ listRecords('at.mapped.activity'),
176176+ ]);
177177+ const data = _buildFromRecs(trailRecs, locationRecs, activityRecs);
178178+ _cachedServiceData = data;
179179+ return data;
180180+}
181181+182182+async function _revalidateServiceData() {
183183+ try {
184184+ const prev = _cachedServiceData;
185185+ const fresh = await _fetchLiveServiceData();
186186+ _serviceInflight = null;
187187+ if (_serviceDataChanged(prev, fresh)) {
188188+ document.dispatchEvent(new CustomEvent('mapped:servicedata'));
189189+ }
190190+ } catch (_) {
191191+ // Silently ignore — stale data is fine
192192+ }
193193+}
194194+195195+function _serviceDataChanged(prev, next) {
196196+ if (!prev) return true;
197197+ return prev.trails.map(t => t.uri).join() !== next.trails.map(t => t.uri).join();
198198+}
199199+155200// ── collectPostRefs ────────────────────────────────────────────────
156201// Discovers all user posts that reference the mapped.at base post via basePost.
157202// Returns an array of { did, rkey } unique post references.
···254299 };
255300}
256301302302+// ── Post cache helpers ─────────────────────────────────────────────
303303+const POST_CACHE_KEY = 'mapped_cache';
304304+const POST_CACHE_TTL_MS = 60_000;
305305+306306+function _readPostsCache() {
307307+ try {
308308+ const raw = localStorage.getItem(POST_CACHE_KEY);
309309+ if (!raw) return null;
310310+ const { posts, cachedAt } = JSON.parse(raw);
311311+ if (Date.now() - cachedAt > POST_CACHE_TTL_MS) return null;
312312+ // Revive Date objects (JSON serialises them as strings)
313313+ return posts.map(p => ({ ...p, timestamp: new Date(p.timestamp) }));
314314+ } catch (_) {
315315+ return null;
316316+ }
317317+}
318318+319319+function _writePostsCache(posts) {
320320+ try {
321321+ localStorage.setItem(POST_CACHE_KEY, JSON.stringify({ posts, cachedAt: Date.now() }));
322322+ } catch (_) {
323323+ // localStorage may be unavailable (private browsing, quota exceeded) — ignore
324324+ }
325325+}
326326+257327// ── fetchAll ───────────────────────────────────────────────────────
258258-// Main entry point. Fetches all mapped.at data and returns normalised arrays.
259259-// Returns a cached Promise — safe to call from multiple components simultaneously.
260260-let _promise = null;
328328+// Main entry point. Returns cached data instantly when available.
329329+// Revalidates posts in background; dispatches 'mapped:posts' on update.
330330+let _cachedResult = null;
331331+let _fetchAllInflight = null;
332332+261333export function fetchAll() {
262262- if (!_promise) _promise = _fetchAll();
263263- return _promise;
334334+ if (_cachedResult) return Promise.resolve(_cachedResult);
335335+ if (!_fetchAllInflight) _fetchAllInflight = _fetchAll();
336336+ return _fetchAllInflight;
264337}
265338266339async function _fetchAll() {
267267- // Step 1: Service account data (cached — shared with TrailsList/TabSwitcher)
268340 const serviceData = await fetchServiceData();
269341 const { trails, locations, activities } = serviceData;
270342271271- // Step 2: Discover posts via basePost backlinks on the mapped.at base post
343343+ // Serve from localStorage cache if fresh
344344+ const cached = _readPostsCache();
345345+ if (cached) {
346346+ _cachedResult = { trails, locations, activities, posts: cached };
347347+ _fetchAllInflight = null;
348348+ _revalidatePosts(serviceData);
349349+ return _cachedResult;
350350+ }
351351+352352+ // Cold fetch
353353+ const posts = await _fetchPosts(serviceData);
354354+ _writePostsCache(posts);
355355+ _cachedResult = { trails, locations, activities, posts };
356356+ return _cachedResult;
357357+}
358358+359359+// Extracted post-fetching logic (constellation + user PDSs).
360360+async function _fetchPosts(serviceData) {
272361 const postRefs = await collectPostRefs();
273362274274- // Step 3: Resolve each unique DID (parallel)
275363 const uniqueDids = [...new Set(postRefs.map(r => r.did))];
276364 const didInfoMap = new Map();
277365 await Promise.all(uniqueDids.map(async did => {
···279367 if (info) didInfoMap.set(did, info);
280368 }));
281369282282- // Step 4: Fetch each post record from its author's PDS (parallel)
283370 const posts = (await Promise.all(postRefs.map(async ({ did, rkey }) => {
284371 const info = didInfoMap.get(did);
285285- if (!info) return null; // DID resolution failed — skip this post
372372+ if (!info) return null;
286373 const value = await fetchPostRecord(info.pds, did, rkey);
287374 if (!value) return null;
288375 return _hydratePost(did, rkey, value, info.author, serviceData);
289376 }))).filter(Boolean);
290377291291- // Sort posts newest first
292378 posts.sort((a, b) => b.timestamp - a.timestamp);
379379+ return posts;
380380+}
293381294294- return { trails, locations, activities, posts };
382382+async function _revalidatePosts(serviceData) {
383383+ try {
384384+ const prevPosts = _cachedResult?.posts ?? [];
385385+ const posts = await _fetchPosts(serviceData);
386386+ _writePostsCache(posts);
387387+ const { trails, locations, activities } = serviceData;
388388+ _cachedResult = { trails, locations, activities, posts };
389389+ if (_postsChanged(prevPosts, posts)) {
390390+ document.dispatchEvent(new CustomEvent('mapped:posts'));
391391+ }
392392+ } catch (_) {
393393+ // Silently ignore — stale data is fine
394394+ }
395395+}
396396+397397+function _postsChanged(prev, next) {
398398+ if (prev.length !== next.length) return true;
399399+ return prev.some((p, i) => p.uri !== next[i].uri || p.timestamp.getTime() !== next[i].timestamp.getTime());
295400}