Linked Designs P1d: autosave restore + publish strip

restoreAutosave now collects every linked layer (top-level + every
deck slide) and kicks off linkedDesigns.resolveLinked for each.
This pre-warms the cache so the first render's renderResolved fires
from a synchronous peek instead of paying the relay round-trip
serially, and duplicate refs across slides dedupe via the inflight
map.

stripLinkedDesignEphemera removes session-only fields
(lastResolvedAt, lastResolvedSize, transient fallbackPreview) from
linked layers in a published payload. Called once at the top of
publishCurrentTemplateToNostr — before the private/premium/public
branches — so every publish path is covered. fallbackPreview only
survives if design.snapshotPreview === true (a future opt-in surface
for designs that want a still preview to ship alongside the link).

Unit tests at /tmp/test_stripLinkedEphemera.js (16 cases) cover the
mutation, opt-in, deck walking, and null-tolerance paths.
This commit is contained in:
thePR0M3TH3AN committed 2026-05-31 21:12:48 -04:00
1 parent a8f7163050
commit a0a7f215c2
1 file changed
+54
+54
View File
@@ -6646,6 +6646,32 @@
return { resolveLinked, walkLinkedTree, walkContributors, clearCache, peek, keyFor, _parseAddressCoord, _cacheKey };
})();
// Pre-publish hook. Linked layers carry render-only fields
// (lastResolvedAt, lastResolvedSize, transient fallbackPreview)
// that are session-local and should not ship in the published
// event. Walks the host payload's layers + every deck slide's
// layers and strips them in place. fallbackPreview survives only
// when the user explicitly opted in via design.snapshotPreview =
// true (a future polish-phase UI surface; not exposed yet).
function stripLinkedDesignEphemera(payload) {
if (!payload || typeof payload !== 'object') return payload;
const strip = (l) => {
if (l?.type !== 'design' || l.design?.kind !== 'linked') return;
delete l.design.lastResolvedAt;
delete l.design.lastResolvedSize;
if (!l.design.snapshotPreview) delete l.design.fallbackPreview;
};
if (Array.isArray(payload.layers)) {
for (const l of payload.layers) strip(l);
}
if (Array.isArray(payload.deck?.slides)) {
for (const s of payload.deck.slides) {
if (Array.isArray(s?.layers)) for (const l of s.layers) strip(l);
}
}
return payload;
}
// ---- NWC sync (cross-device wallet credential sharing) -----------------
// Stores the NWC connection URI in a NIP-44-self-encrypted kind-30078
// event with d=artstr:nwc-connection:v1. Mirrors the purchase-vault
@@ -15428,6 +15454,11 @@
throw new Error('Add cover artwork or at least one layer before publishing.');
}
}
// Linked-design layers carry session-only fields (lastResolvedAt,
// lastResolvedSize, transient fallbackPreview) that must not ship
// in the published event. Strip them once here, before the path
// branches into private/premium/public publish.
stripLinkedDesignEphemera(payload);
// Private: encrypt and publish a feed-invisible event instead of the
// normal public, tagged one.
if (state.visibility === 'private') {
@@ -26868,6 +26899,29 @@
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.
const linkedSpecs = [];
const collect = (layers) => {
if (!Array.isArray(layers)) return;
for (const l of layers) {
if (l?.type === 'design' && l.design?.kind === 'linked' && l.design.ref) {
linkedSpecs.push({ ref: l.design.ref, pinnedEventId: l.design.pinnedEventId || null });
}
}
};
collect(state.layers);
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 {}
}
} catch (error) {
console.warn('Autosave restore failed', error);
}