mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
Linked Designs P3d: batched prefetch on cold start
Last Phase 3 polish piece per LINKED_DESIGNS_FEATURE.md §14. The cold-start path used to fire one parallel queryRelays per linked layer — a host with 20 distinct linked sources opened 20 sockets to each relay in the union. Now restoreAutosave issues one batched relay round-trip via the new linkedDesigns.prefetchBatch. Mechanics: 1. queryRelays accepts either a single filter object (existing callers) or an array of filters; arrays spread into the NIP-01 REQ after the sub id (OR semantics). One-line change at the send site; zero impact on existing call sites. 2. prefetchBatch(specs) skips anything already in the in-memory cache, inflight map, or disk cache — partial cache hits only fetch what's missing. Distinct address-coord refs are grouped by (kind, pubkey) into one filter each (#d set of dTags); pinned event ids collapse into a single ids-filter. Events bucket by id (for pinned matches) and by address coord with latest created_at wins (for ref matches). 3. restoreAutosave's per-spec resolveLinked loop becomes a single prefetchBatch call. The disk-cache pre-filter inside prefetchBatch means a reload of a previously-resolved host pays zero network cost; only newly-linked refs hit the wire. Per-layer resolveLinked calls in the renderer still work the way they did — this only changes the bulk warm-up path. Interactive adds (one new linked layer at a time) keep their single-filter fast path.
This commit is contained in:
1 parent
81e33fab9f
commit
8439b358c3
1 file changed
+107
-11
+107
-11
@@ -6856,6 +6856,98 @@
|
||||
|
||||
function keyFor(linkSpec) { return _cacheKey(linkSpec || {}); }
|
||||
|
||||
// Fetch many linked specs in one round-trip. Combines all
|
||||
// distinct address-coord refs into a multi-filter REQ
|
||||
// (grouped by author + kind for relay-side efficiency) and
|
||||
// any pinned event ids into a single ids-filter. Skips
|
||||
// anything already in the in-memory or disk cache so a
|
||||
// partial cache hit only fetches what's actually missing.
|
||||
// Populates both caches on completion.
|
||||
//
|
||||
// Used by restoreAutosave to warm the cache for every
|
||||
// linked layer in a saved design in one relay round-trip
|
||||
// instead of N parallel queries.
|
||||
async function prefetchBatch(specs) {
|
||||
if (!Array.isArray(specs) || !specs.length) return;
|
||||
// Resolve which specs actually need a network fetch.
|
||||
const needFetch = [];
|
||||
const seenKeys = new Set();
|
||||
for (const spec of specs) {
|
||||
if (!spec) continue;
|
||||
const key = _cacheKey(spec);
|
||||
if (!key || seenKeys.has(key)) continue;
|
||||
seenKeys.add(key);
|
||||
if (_fromCache(key)) continue;
|
||||
if (inflight.has(key)) continue;
|
||||
if (_diskRead(key)) continue;
|
||||
needFetch.push({ key, spec });
|
||||
}
|
||||
if (!needFetch.length) return;
|
||||
// Build batched filters.
|
||||
const idList = [];
|
||||
const byAuthorKind = new Map(); // "kind:pubkey" → Set<dTag>
|
||||
for (const { spec } of needFetch) {
|
||||
if (spec.pinnedEventId && /^[0-9a-f]{64}$/i.test(spec.pinnedEventId)) {
|
||||
idList.push(spec.pinnedEventId);
|
||||
continue;
|
||||
}
|
||||
const coord = _parseAddressCoord(spec.ref);
|
||||
if (!coord) continue;
|
||||
const k = `${coord.kind}:${coord.pubkey}`;
|
||||
let set = byAuthorKind.get(k);
|
||||
if (!set) { set = new Set(); byAuthorKind.set(k, set); }
|
||||
set.add(coord.dTag);
|
||||
}
|
||||
const filters = [];
|
||||
if (idList.length) filters.push({ ids: [...new Set(idList)], limit: idList.length });
|
||||
for (const [k, dTags] of byAuthorKind) {
|
||||
const [kindStr, pubkey] = k.split(':');
|
||||
filters.push({
|
||||
kinds: [Number(kindStr)],
|
||||
authors: [pubkey],
|
||||
'#d': [...dTags],
|
||||
limit: dTags.size * 2,
|
||||
});
|
||||
}
|
||||
if (!filters.length) return;
|
||||
const relays = typeof _vaultRelayUnion === 'function'
|
||||
? _vaultRelayUnion()
|
||||
: splitRelayList(state.community?.relays || '');
|
||||
let events;
|
||||
try {
|
||||
events = await queryRelays(relays, filters, { timeoutMs: 4500 });
|
||||
} catch (err) {
|
||||
console.warn('[linked-designs] batched prefetch failed', err);
|
||||
return;
|
||||
}
|
||||
// Bucket events: by id (for pinned lookups) and by address
|
||||
// coord (for ref lookups; latest created_at wins).
|
||||
const byId = new Map();
|
||||
const byAddr = new Map();
|
||||
for (const ev of (events || [])) {
|
||||
byId.set(ev.id, ev);
|
||||
const dTag = (ev.tags || []).find(t => t[0] === 'd' && t[1] != null)?.[1] || '';
|
||||
const addr = `${ev.kind}:${ev.pubkey}:${dTag}`;
|
||||
const prev = byAddr.get(addr);
|
||||
if (!prev || (ev.created_at || 0) > (prev.created_at || 0)) byAddr.set(addr, ev);
|
||||
}
|
||||
// Match each requested spec to an event + seed caches.
|
||||
for (const { key, spec } of needFetch) {
|
||||
let ev = null;
|
||||
if (spec.pinnedEventId) {
|
||||
ev = byId.get(spec.pinnedEventId) || null;
|
||||
} else {
|
||||
const coord = _parseAddressCoord(spec.ref);
|
||||
if (coord) ev = byAddr.get(`${coord.kind}:${coord.pubkey}:${coord.dTag}`) || null;
|
||||
}
|
||||
if (!ev) continue;
|
||||
const entry = await _entryFromEvent(ev);
|
||||
if (!entry) continue;
|
||||
cache.set(key, { ...entry, fetchedAt: _now() });
|
||||
_diskWrite(key, ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate both caches from a freshly-published source event
|
||||
// so a layer that was just extracted-and-linked paints
|
||||
// instantly without waiting for relay propagation. Returns
|
||||
@@ -6871,7 +6963,7 @@
|
||||
return entry;
|
||||
}
|
||||
|
||||
return { resolveLinked, walkLinkedTree, walkContributors, clearCache, peek, keyFor, seed, _parseAddressCoord, _cacheKey };
|
||||
return { resolveLinked, walkLinkedTree, walkContributors, clearCache, peek, keyFor, seed, prefetchBatch, _parseAddressCoord, _cacheKey };
|
||||
})();
|
||||
|
||||
// Walk the host's linked tree to collect (a) distinct contributor
|
||||
@@ -8196,7 +8288,11 @@
|
||||
const subId = `sub-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
||||
ws.addEventListener('open', () => {
|
||||
try {
|
||||
ws.send(JSON.stringify(['REQ', subId, filters]));
|
||||
// NIP-01 REQ accepts multiple filters spread after the
|
||||
// sub id (OR semantics). Callers can pass a single
|
||||
// filter object (legacy) or an array; both work.
|
||||
const filterList = Array.isArray(filters) ? filters : [filters];
|
||||
ws.send(JSON.stringify(['REQ', subId, ...filterList]));
|
||||
} catch {
|
||||
clearTimeout(timer);
|
||||
markRelayFailure(relay, 'Could not send query', { context: 'read' });
|
||||
@@ -27862,13 +27958,13 @@
|
||||
if ((parsed.appMode || parsed.ui?.appMode) === 'designer') {
|
||||
loadCurrentDiscDesignLayers();
|
||||
}
|
||||
// Kick off the linked-design resolver for every restored
|
||||
// linked layer so the cache warms in parallel with the first
|
||||
// render. The renderer's async branch handles the empty-cache
|
||||
// case correctly on its own, but pre-warming dedupes
|
||||
// duplicate refs across slides via the inflight map and lets
|
||||
// the slot's renderResolved fire from a synchronous peek on
|
||||
// subsequent re-renders. Non-blocking.
|
||||
// Warm the linked-design cache for every restored linked
|
||||
// layer in one batched relay round-trip (instead of N
|
||||
// parallel queries). prefetchBatch checks the disk cache
|
||||
// first and only fetches refs that are actually missing,
|
||||
// so common reload scenarios pay zero network cost.
|
||||
// Non-blocking; the renderer's async branch picks up the
|
||||
// freshly-warmed cache on first paint.
|
||||
const linkedSpecs = [];
|
||||
const collect = (layers) => {
|
||||
if (!Array.isArray(layers)) return;
|
||||
@@ -27882,8 +27978,8 @@
|
||||
if (Array.isArray(state.deck?.slides)) {
|
||||
for (const s of state.deck.slides) collect(s?.layers);
|
||||
}
|
||||
for (const spec of linkedSpecs) {
|
||||
try { linkedDesigns.resolveLinked(spec); } catch {}
|
||||
if (linkedSpecs.length) {
|
||||
try { linkedDesigns.prefetchBatch(linkedSpecs); } catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Autosave restore failed', error);
|
||||
|
||||
Reference in new issue
Block a user