mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Sync generated provider output
This commit is contained in:
@@ -64,6 +64,26 @@
|
||||
};
|
||||
}
|
||||
|
||||
function hasFrameworkHmrOwnership(el) {
|
||||
for (let node = el; node; node = node.parentElement) {
|
||||
let keys = [];
|
||||
try { keys = Object.getOwnPropertyNames(node); } catch {}
|
||||
if (keys.some((key) => (
|
||||
key.startsWith('__reactFiber$')
|
||||
|| key.startsWith('__reactProps$')
|
||||
|| key.startsWith('__reactContainer$')
|
||||
|| key === '_reactRootContainer'
|
||||
|| key === '__vueParentComponent'
|
||||
|| key === '__vue_app__'
|
||||
|| key === '__vnode'
|
||||
|| key === '__svelte_meta'
|
||||
))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function id8() {
|
||||
if (crypto?.randomUUID) return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
||||
return (Math.random().toString(16).slice(2) + Date.now().toString(16)).slice(0, 8);
|
||||
@@ -128,6 +148,7 @@
|
||||
desc,
|
||||
rectIsUsableAnchor,
|
||||
makeFrozenAnchor,
|
||||
hasFrameworkHmrOwnership,
|
||||
id8,
|
||||
cssId,
|
||||
liveUiRoot,
|
||||
|
||||
@@ -71,17 +71,38 @@
|
||||
return checkpointRevision;
|
||||
}
|
||||
|
||||
function readHandledIds() {
|
||||
const raw = safeRead(handledKey);
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.filter(id => typeof id === 'string' && id);
|
||||
}
|
||||
if (typeof parsed === 'string' && parsed) return [parsed];
|
||||
} catch { /* legacy values were stored as a plain session id */ }
|
||||
return [raw];
|
||||
}
|
||||
|
||||
function markHandled(id) {
|
||||
if (!id) return;
|
||||
safeWrite(handledKey, id);
|
||||
const ids = readHandledIds().filter(existing => existing !== id);
|
||||
ids.push(id);
|
||||
safeWrite(handledKey, JSON.stringify(ids.slice(-8)));
|
||||
}
|
||||
|
||||
function isHandled(id) {
|
||||
return !!id && safeRead(handledKey) === id;
|
||||
return !!id && readHandledIds().includes(id);
|
||||
}
|
||||
|
||||
function clearHandled() {
|
||||
safeRemove(handledKey);
|
||||
function clearHandled(id) {
|
||||
if (!id) {
|
||||
safeRemove(handledKey);
|
||||
return;
|
||||
}
|
||||
const remaining = readHandledIds().filter(existing => existing !== id);
|
||||
if (remaining.length > 0) safeWrite(handledKey, JSON.stringify(remaining));
|
||||
else safeRemove(handledKey);
|
||||
}
|
||||
|
||||
function writeScrollY(y) {
|
||||
|
||||
@@ -121,6 +121,10 @@
|
||||
let hoveredElement = null;
|
||||
let selectedElement = null;
|
||||
let currentSessionId = null;
|
||||
// Advances when the user begins configuring a fresh edit, before that edit
|
||||
// has a server session id. Deferred recovery captures this revision so an
|
||||
// older accept/discard can never reload over a replacement configuration.
|
||||
let liveInteractionRevision = 0;
|
||||
let expectedVariants = 0;
|
||||
let arrivedVariants = 0;
|
||||
let visibleVariant = 0;
|
||||
@@ -188,6 +192,9 @@
|
||||
// when the real accept result arrives or a new session starts.
|
||||
let awaitingAcceptResult = null;
|
||||
let variantObserver = null;
|
||||
const discardedFrameworkWrapperWatchers = new Map();
|
||||
const handledRuntimeWrapperWatchers = new Map();
|
||||
const handledRuntimeWrapperReloadSessions = new Set();
|
||||
let variantSelectionInFlight = false;
|
||||
let variantSelectionPromise = null;
|
||||
let recoveringEmptyCycling = false;
|
||||
@@ -208,6 +215,7 @@
|
||||
const SCROLL_ANCHOR_LOCK_ID = 'impeccable-scroll-anchor-lock';
|
||||
const VARIANT_STATE_STYLE_ID = 'impeccable-variant-state';
|
||||
const DISCARD_STATE_STYLE_ID = 'impeccable-discard-state';
|
||||
const HANDLED_WRAPPER_RELOAD_KEY = PREFIX + '-handled-wrapper-reload';
|
||||
|
||||
// Dedicated key for scroll position - SEPARATE from LS_KEY so that
|
||||
// saveSession's state updates don't clobber a carefully-captured scrollY.
|
||||
@@ -270,6 +278,7 @@
|
||||
desc,
|
||||
rectIsUsableAnchor,
|
||||
makeFrozenAnchor,
|
||||
hasFrameworkHmrOwnership,
|
||||
id8,
|
||||
cssId,
|
||||
liveUiRoot,
|
||||
@@ -2034,6 +2043,16 @@
|
||||
syncSteerQueueHint();
|
||||
}
|
||||
|
||||
function beginNewLiveConfiguration() {
|
||||
liveInteractionRevision += 1;
|
||||
setLiveState('CONFIGURING');
|
||||
}
|
||||
|
||||
function deferredRecoverySuperseded(sessionId, recoveryRevision) {
|
||||
return liveInteractionRevision !== recoveryRevision
|
||||
|| !!(currentSessionId && currentSessionId !== sessionId);
|
||||
}
|
||||
|
||||
/** Element used to position the floating bar / shader during a session. */
|
||||
function resolveBarAnchor() {
|
||||
if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {
|
||||
@@ -5036,7 +5055,7 @@
|
||||
&& el.parentElement
|
||||
&& document.body.contains(el)
|
||||
&& !own(el)
|
||||
&& !el.closest?.('[data-impeccable-variants]');
|
||||
&& !el.closest?.('[data-impeccable-variants],[data-impeccable-carbonize]');
|
||||
}
|
||||
|
||||
function elementMatchesOriginalMarkup(liveEl, origContent) {
|
||||
@@ -6608,19 +6627,78 @@
|
||||
document.getElementById(VARIANT_STATE_STYLE_ID)?.remove();
|
||||
}
|
||||
|
||||
function discardStateStyleId(sessionId) {
|
||||
return DISCARD_STATE_STYLE_ID + '-' + sessionId;
|
||||
}
|
||||
|
||||
function showOriginalDuringDiscard(sessionId) {
|
||||
if (!sessionId) return;
|
||||
let styleEl = document.getElementById(DISCARD_STATE_STYLE_ID);
|
||||
let styleEl = document.getElementById(discardStateStyleId(sessionId));
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style');
|
||||
styleEl.id = DISCARD_STATE_STYLE_ID;
|
||||
styleEl.id = discardStateStyleId(sessionId);
|
||||
(document.head || document.documentElement).appendChild(styleEl);
|
||||
}
|
||||
styleEl.dataset.impeccableDiscardSession = sessionId;
|
||||
const wrapper = '[data-impeccable-variants="' + sessionId + '"]';
|
||||
styleEl.textContent = wrapper + ' > [data-impeccable-variant]:not([data-impeccable-variant="original"]) { display:none !important; }\n'
|
||||
+ wrapper + ' > [data-impeccable-variant="original"] { display:block !important; }';
|
||||
}
|
||||
|
||||
function removeDiscardStateStylesheet(sessionId) {
|
||||
if (!sessionId) return;
|
||||
document.getElementById(discardStateStyleId(sessionId))?.remove();
|
||||
}
|
||||
|
||||
function releaseDiscardedStaticWrapper(wrapper, sessionId) {
|
||||
removeDiscardStateStylesheet(sessionId);
|
||||
if (!wrapper) return;
|
||||
const orig = wrapper.querySelector('[data-impeccable-variant="original"]');
|
||||
const content = orig?.firstElementChild;
|
||||
if (content && wrapper.parentElement) {
|
||||
wrapper.parentElement.replaceChild(content, wrapper);
|
||||
return;
|
||||
}
|
||||
wrapper.remove();
|
||||
}
|
||||
|
||||
function watchForDiscardedFrameworkWrapperRemoval(sessionId) {
|
||||
if (!sessionId || !document.body) return;
|
||||
if (discardedFrameworkWrapperWatchers.has(sessionId)) return;
|
||||
const selector = '[data-impeccable-variants="' + sessionId + '"]';
|
||||
let observer = null;
|
||||
let timer = null;
|
||||
const stopWatching = function() {
|
||||
observer?.disconnect();
|
||||
if (timer) clearTimeout(timer);
|
||||
discardedFrameworkWrapperWatchers.delete(sessionId);
|
||||
};
|
||||
const finishIfGone = function() {
|
||||
if (document.querySelector(selector)) return false;
|
||||
removeDiscardStateStylesheet(sessionId);
|
||||
stopWatching();
|
||||
return true;
|
||||
};
|
||||
if (finishIfGone()) return;
|
||||
observer = new MutationObserver(finishIfGone);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
const resolveStillMounted = function() {
|
||||
if (finishIfGone()) return;
|
||||
const replacementActive = !!currentSessionId
|
||||
|| (state !== 'IDLE' && state !== 'PICKING');
|
||||
if (replacementActive) {
|
||||
timer = setTimeout(resolveStillMounted, 12000);
|
||||
discardedFrameworkWrapperWatchers.get(sessionId).timer = timer;
|
||||
return;
|
||||
}
|
||||
removeDiscardStateStylesheet(sessionId);
|
||||
stopWatching();
|
||||
location.reload();
|
||||
};
|
||||
timer = setTimeout(resolveStillMounted, 12000);
|
||||
discardedFrameworkWrapperWatchers.set(sessionId, { observer, timer });
|
||||
}
|
||||
|
||||
function resolveScrollLockAnchorTop() {
|
||||
const anchor = resolveBarAnchor();
|
||||
if (!anchor?.isConnected) return null;
|
||||
@@ -7335,7 +7413,7 @@
|
||||
hideInsertLine();
|
||||
configureKind = 'insert';
|
||||
selectedElement = placeholder;
|
||||
setLiveState('CONFIGURING');
|
||||
beginNewLiveConfiguration();
|
||||
hideHighlight();
|
||||
clearAnnotations();
|
||||
showAnnotOverlay(placeholder);
|
||||
@@ -7353,7 +7431,7 @@
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectedElement = hoveredElement;
|
||||
setLiveState('CONFIGURING');
|
||||
beginNewLiveConfiguration();
|
||||
showHighlight(selectedElement);
|
||||
clearAnnotations();
|
||||
showAnnotOverlay(selectedElement);
|
||||
@@ -7530,7 +7608,7 @@
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
selectedElement = hoveredElement;
|
||||
setLiveState('CONFIGURING');
|
||||
beginNewLiveConfiguration();
|
||||
showHighlight(selectedElement);
|
||||
clearAnnotations();
|
||||
showAnnotOverlay(selectedElement);
|
||||
@@ -8535,6 +8613,7 @@ void main() {
|
||||
}
|
||||
|
||||
function scheduleAcceptCleanup(accepted) {
|
||||
const recoveryRevision = liveInteractionRevision;
|
||||
queueMicrotask(function() {
|
||||
if (pendingAcceptedSession?.id !== accepted?.id) return;
|
||||
// Svelte previews live in an adapter-owned mount rather than in source
|
||||
@@ -8551,8 +8630,10 @@ void main() {
|
||||
// races. Static servers still need a fallback, but it must not keep Live
|
||||
// in SAVING or block the user's next pick.
|
||||
if (!accepted?.isSvelteComponent) {
|
||||
watchForHandledRuntimeWrapper(accepted?.id, recoveryRevision);
|
||||
setTimeout(function() {
|
||||
if (!acceptedDomAlreadyClean(accepted)) ensureAcceptedDomClean(accepted);
|
||||
if (deferredRecoverySuperseded(accepted?.id, recoveryRevision)) return;
|
||||
if (!acceptedDomAlreadyClean(accepted)) ensureAcceptedDomClean(accepted, recoveryRevision);
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
@@ -8585,13 +8666,27 @@ void main() {
|
||||
&& matches.every((el) => !el.closest('[data-impeccable-variants],[data-impeccable-variant],[data-impeccable-carbonize]'));
|
||||
}
|
||||
|
||||
function ensureAcceptedDomClean(pending) {
|
||||
function ensureAcceptedDomClean(pending, recoveryRevision) {
|
||||
// Background cleanup for an accepted session must never mutate or reload
|
||||
// a newer comparison the user has already started.
|
||||
if (deferredRecoverySuperseded(pending?.id, recoveryRevision)) return;
|
||||
if (acceptedDomAlreadyClean(pending)) return;
|
||||
const sessionId = pending?.id;
|
||||
const variantId = pending?.variant;
|
||||
const wrappers = findAcceptedRuntimeWrappers(sessionId);
|
||||
if (hasFrameworkHmrOwnership(wrappers[0] || pending?.parentElement)) {
|
||||
// Vite can coalesce rapid scaffold/carbonize writes and leave the last
|
||||
// framework-owned preview tree mounted even though source is clean. Give
|
||||
// HMR another grace window, then reload from clean source rather than
|
||||
// violating reconciler ownership with a manual DOM mutation.
|
||||
setTimeout(function() {
|
||||
if (deferredRecoverySuperseded(pending?.id, recoveryRevision)) return;
|
||||
if (!acceptedDomAlreadyClean(pending)) location.reload();
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
if (wrappers.length === 0) {
|
||||
restoreAcceptedDomFromSnapshot(pending);
|
||||
restoreAcceptedDomFromSnapshot(pending, recoveryRevision);
|
||||
return;
|
||||
}
|
||||
for (const wrapper of wrappers) {
|
||||
@@ -8608,7 +8703,7 @@ void main() {
|
||||
}
|
||||
wrapper.remove();
|
||||
}
|
||||
if (!acceptedDomAlreadyClean(pending)) restoreAcceptedDomFromSnapshot(pending);
|
||||
if (!acceptedDomAlreadyClean(pending)) restoreAcceptedDomFromSnapshot(pending, recoveryRevision);
|
||||
}
|
||||
|
||||
function findAcceptedRuntimeWrappers(sessionId) {
|
||||
@@ -8619,17 +8714,17 @@ void main() {
|
||||
])];
|
||||
}
|
||||
|
||||
function restoreAcceptedDomFromSnapshot(pending) {
|
||||
function restoreAcceptedDomFromSnapshot(pending, recoveryRevision) {
|
||||
if (acceptedDomAlreadyClean(pending)) return;
|
||||
if (!pending?.acceptedHtml) {
|
||||
reloadAfterMissingAcceptedDom(pending);
|
||||
reloadAfterMissingAcceptedDom(pending, recoveryRevision);
|
||||
return;
|
||||
}
|
||||
const parent = pending.parentElement?.isConnected
|
||||
? pending.parentElement
|
||||
: (pending.parentSelector ? document.querySelector(pending.parentSelector) : null);
|
||||
if (!parent) {
|
||||
reloadAfterMissingAcceptedDom(pending);
|
||||
reloadAfterMissingAcceptedDom(pending, recoveryRevision);
|
||||
return;
|
||||
}
|
||||
const template = document.createElement('template');
|
||||
@@ -8638,10 +8733,11 @@ void main() {
|
||||
? pending.nextSibling
|
||||
: null;
|
||||
parent.insertBefore(template.content, anchor);
|
||||
if (!acceptedDomAlreadyClean(pending)) reloadAfterMissingAcceptedDom(pending);
|
||||
if (!acceptedDomAlreadyClean(pending)) reloadAfterMissingAcceptedDom(pending, recoveryRevision);
|
||||
}
|
||||
|
||||
function reloadAfterMissingAcceptedDom(pending) {
|
||||
function reloadAfterMissingAcceptedDom(pending, recoveryRevision) {
|
||||
if (deferredRecoverySuperseded(pending?.id, recoveryRevision)) return;
|
||||
if (acceptedDomAlreadyClean(pending)) return;
|
||||
if (pending?.id && document.querySelector('[data-impeccable-variants="' + pending.id + '"]')) return;
|
||||
location.reload();
|
||||
@@ -8991,14 +9087,15 @@ void main() {
|
||||
return sessionState.isHandled(id);
|
||||
}
|
||||
|
||||
function clearHandled() {
|
||||
sessionState.clearHandled();
|
||||
function clearHandled(sessionId) {
|
||||
sessionState.clearHandled(sessionId);
|
||||
}
|
||||
|
||||
function cleanup(options) {
|
||||
const restoreOriginal = options?.restoreOriginal === true;
|
||||
const instantChrome = options?.instantChrome === true;
|
||||
const cleanupSessionId = currentSessionId;
|
||||
const cleanupRevision = liveInteractionRevision;
|
||||
clearMountErrorCard();
|
||||
lastReportedMountFailure = null;
|
||||
if (svelteComponentSession?.sessionId === cleanupSessionId) {
|
||||
@@ -9016,19 +9113,41 @@ void main() {
|
||||
else wrapper.style.display = 'none';
|
||||
}
|
||||
setTimeout(function() {
|
||||
document.getElementById(DISCARD_STATE_STYLE_ID)?.remove();
|
||||
if (!cleanupSessionId) return;
|
||||
const lateWrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
||||
if (!lateWrapper) return;
|
||||
const orig = lateWrapper.querySelector('[data-impeccable-variant="original"]');
|
||||
if (orig) {
|
||||
const content = orig.firstElementChild;
|
||||
if (content) {
|
||||
lateWrapper.parentElement.replaceChild(content, lateWrapper);
|
||||
return;
|
||||
}
|
||||
const recoverySuperseded = deferredRecoverySuperseded(cleanupSessionId, cleanupRevision);
|
||||
if (!cleanupSessionId) {
|
||||
removeDiscardStateStylesheet();
|
||||
return;
|
||||
}
|
||||
lateWrapper.remove();
|
||||
const lateWrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
||||
if (!lateWrapper) {
|
||||
removeDiscardStateStylesheet(cleanupSessionId);
|
||||
return;
|
||||
}
|
||||
if (recoverySuperseded) {
|
||||
if (hasFrameworkHmrOwnership(lateWrapper)) {
|
||||
watchForDiscardedFrameworkWrapperRemoval(cleanupSessionId);
|
||||
} else {
|
||||
releaseDiscardedStaticWrapper(lateWrapper, cleanupSessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (hasFrameworkHmrOwnership(lateWrapper)) {
|
||||
// As on accept, never restructure framework-owned DOM. If HMR missed
|
||||
// the final source rewrite, reload once after a grace window so the
|
||||
// discarded source becomes authoritative without a reconciler race.
|
||||
setTimeout(function() {
|
||||
const staleWrapper = document.querySelector('[data-impeccable-variants="' + cleanupSessionId + '"]');
|
||||
if (deferredRecoverySuperseded(cleanupSessionId, cleanupRevision)) {
|
||||
if (!staleWrapper) removeDiscardStateStylesheet(cleanupSessionId);
|
||||
else watchForDiscardedFrameworkWrapperRemoval(cleanupSessionId);
|
||||
return;
|
||||
}
|
||||
removeDiscardStateStylesheet(cleanupSessionId);
|
||||
if (staleWrapper) location.reload();
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
releaseDiscardedStaticWrapper(lateWrapper, cleanupSessionId);
|
||||
}, 2000);
|
||||
}
|
||||
hideBar(instantChrome);
|
||||
@@ -9111,12 +9230,122 @@ void main() {
|
||||
// Resume an active variant session after HMR/page reload.
|
||||
// If a [data-impeccable-variants] wrapper exists in the DOM, the agent wrote
|
||||
// variants before HMR fired. Pick up where we left off.
|
||||
function resumeSession() {
|
||||
function handledWrapperReloadKey(sessionId) {
|
||||
return HANDLED_WRAPPER_RELOAD_KEY + ':' + sessionId;
|
||||
}
|
||||
|
||||
function clearHandledWrapperReloadStamp(sessionId) {
|
||||
try {
|
||||
if (sessionId) {
|
||||
sessionStorage.removeItem(handledWrapperReloadKey(sessionId));
|
||||
const legacy = sessionStorage.getItem(HANDLED_WRAPPER_RELOAD_KEY) || '';
|
||||
if (legacy === sessionId || legacy.startsWith(sessionId + ':')) {
|
||||
sessionStorage.removeItem(HANDLED_WRAPPER_RELOAD_KEY);
|
||||
}
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem(HANDLED_WRAPPER_RELOAD_KEY);
|
||||
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key?.startsWith(HANDLED_WRAPPER_RELOAD_KEY + ':')) sessionStorage.removeItem(key);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function scheduleHandledRuntimeWrapperReload(wrapper, recoveryRevision = liveInteractionRevision) {
|
||||
const sessionId = wrapper?.dataset?.impeccableVariants
|
||||
|| wrapper?.dataset?.impeccableCarbonize;
|
||||
if (!sessionId || !isSessionHandled(sessionId)) return false;
|
||||
if (deferredRecoverySuperseded(sessionId, recoveryRevision)) return true;
|
||||
|
||||
if (handledRuntimeWrapperReloadSessions.has(sessionId)) return true;
|
||||
let reloadAttempts = 0;
|
||||
try {
|
||||
reloadAttempts = Number(sessionStorage.getItem(handledWrapperReloadKey(sessionId))) || 0;
|
||||
if (reloadAttempts >= 2) return true;
|
||||
sessionStorage.setItem(handledWrapperReloadKey(sessionId), String(reloadAttempts + 1));
|
||||
} catch {}
|
||||
handledRuntimeWrapperReloadSessions.add(sessionId);
|
||||
|
||||
// A framework refresh can replace the variants tree with an intermediate
|
||||
// carbonize tree and reload the page, cancelling the original accept timer.
|
||||
// Let the file-side cleanup settle, then reload once from authoritative
|
||||
// source. The sessionStorage stamp prevents a stale dev-server response
|
||||
// from turning this recovery into a reload loop.
|
||||
setTimeout(function() {
|
||||
if (deferredRecoverySuperseded(sessionId, recoveryRevision)) {
|
||||
clearHandledWrapperReloadStamp(sessionId);
|
||||
handledRuntimeWrapperReloadSessions.delete(sessionId);
|
||||
return;
|
||||
}
|
||||
const staleWrapper = document.querySelector(
|
||||
'[data-impeccable-variants="' + sessionId + '"],'
|
||||
+ '[data-impeccable-carbonize="' + sessionId + '"]',
|
||||
);
|
||||
if (staleWrapper) location.reload();
|
||||
else {
|
||||
clearHandledWrapperReloadStamp(sessionId);
|
||||
handledRuntimeWrapperReloadSessions.delete(sessionId);
|
||||
}
|
||||
}, 3000);
|
||||
return true;
|
||||
}
|
||||
|
||||
function watchForHandledRuntimeWrapper(sessionId, recoveryRevision = liveInteractionRevision) {
|
||||
if (!sessionId || !document.body) return;
|
||||
const existing = handledRuntimeWrapperWatchers.get(sessionId);
|
||||
existing?.observer.disconnect();
|
||||
if (existing?.timer) clearTimeout(existing.timer);
|
||||
|
||||
const findHandledWrapper = function() {
|
||||
const wrapper = document.querySelector(
|
||||
'[data-impeccable-variants="' + sessionId + '"],'
|
||||
+ '[data-impeccable-carbonize="' + sessionId + '"]',
|
||||
);
|
||||
if (wrapper) scheduleHandledRuntimeWrapperReload(wrapper, recoveryRevision);
|
||||
};
|
||||
|
||||
// Vite can briefly render the clean accepted tree, then apply a delayed
|
||||
// carbonize refresh after the one-shot accept fallback has already passed.
|
||||
// Keep a bounded scout alive through that refresh window so a late stale
|
||||
// framework tree still reloads from the now-authoritative source.
|
||||
const observer = new MutationObserver(findHandledWrapper);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
const timer = setTimeout(function() {
|
||||
if (handledRuntimeWrapperWatchers.get(sessionId)?.observer !== observer) return;
|
||||
observer.disconnect();
|
||||
handledRuntimeWrapperWatchers.delete(sessionId);
|
||||
}, 12000);
|
||||
handledRuntimeWrapperWatchers.set(sessionId, { observer, timer });
|
||||
findHandledWrapper();
|
||||
}
|
||||
|
||||
function restoreSessionSupersedingHandledWrapper(runtimeWrapper) {
|
||||
const handledSessionId = runtimeWrapper?.dataset?.impeccableVariants
|
||||
|| runtimeWrapper?.dataset?.impeccableCarbonize;
|
||||
if (!handledSessionId || !isSessionHandled(handledSessionId)) return false;
|
||||
|
||||
// Accept releases the picker before carbonize finishes, so a replacement
|
||||
// generation can already be durable while the prior handled wrapper is
|
||||
// still mounted. Restore that newer session before the stale-wrapper
|
||||
// recovery path gets a chance to reload or consume its retry budget.
|
||||
const saved = loadSession();
|
||||
if (!saved?.id || saved.id === handledSessionId || isSessionHandled(saved.id)) return false;
|
||||
if (currentSessionId === saved.id) return true;
|
||||
return restoreSessionWithoutWrapper('browser_resumed_over_handled_wrapper');
|
||||
}
|
||||
|
||||
function resumeSession(recoveryRevision = liveInteractionRevision) {
|
||||
const wrapper = document.querySelector('[data-impeccable-variants]');
|
||||
const runtimeWrapper = wrapper || document.querySelector('[data-impeccable-carbonize]');
|
||||
if (restoreSessionSupersedingHandledWrapper(runtimeWrapper)) return true;
|
||||
if (scheduleHandledRuntimeWrapperReload(runtimeWrapper, recoveryRevision)) return false;
|
||||
if (!wrapper) {
|
||||
if (restoreSessionWithoutWrapper('browser_resumed_without_wrapper')) return true;
|
||||
clearSession();
|
||||
clearHandled();
|
||||
// Keep the bounded handled-id history durable. A framework can hydrate a
|
||||
// completed wrapper well after initialization, and a later reload must
|
||||
// still recognize that wrapper as recovery work rather than resume it.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9136,7 +9365,7 @@ void main() {
|
||||
wrapper.remove();
|
||||
if (restoreSessionWithoutWrapper('browser_resumed_svelte_orphan_wrapper')) return true;
|
||||
clearSession();
|
||||
clearHandled();
|
||||
clearHandled(sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12520,22 +12749,28 @@ void main() {
|
||||
connectSSE();
|
||||
|
||||
// Check for an active session to resume (variant wrapper already in DOM after HMR)
|
||||
if (!resumeSession()) {
|
||||
const resumed = resumeSession();
|
||||
if (!resumed) {
|
||||
console.log('[impeccable] Live variant mode ready. Hover over elements to pick one.');
|
||||
// SvelteKit (and any framework that hydrates after HTML parse) may add
|
||||
// the variant wrapper AFTER init runs. Watch for it and retry resume
|
||||
// once it appears. Disconnect on first hit.
|
||||
} else {
|
||||
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
|
||||
}
|
||||
|
||||
// SvelteKit, React, and other frameworks may restore a durable session
|
||||
// before hydration adds its variant wrapper. Keep a deferred-wrapper scout
|
||||
// whenever init did not see a runtime wrapper, even if local/server state
|
||||
// was already restored successfully. Disconnect on the first wrapper hit.
|
||||
if (!resumed || !document.querySelector('[data-impeccable-variants],[data-impeccable-carbonize]')) {
|
||||
const deferredResumeRevision = liveInteractionRevision;
|
||||
const scout = new MutationObserver(() => {
|
||||
const wrapper = document.querySelector('[data-impeccable-variants]');
|
||||
const wrapper = document.querySelector('[data-impeccable-variants],[data-impeccable-carbonize]');
|
||||
if (!wrapper) return;
|
||||
scout.disconnect();
|
||||
if (resumeSession()) {
|
||||
if (resumeSession(deferredResumeRevision)) {
|
||||
console.log('[impeccable] Resumed deferred session ' + currentSessionId + ' (post-hydration).');
|
||||
}
|
||||
});
|
||||
scout.observe(document.body, { childList: true, subtree: true });
|
||||
} else {
|
||||
console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).');
|
||||
}
|
||||
|
||||
if (state === 'IDLE' && (pickActive || insertActive)) setLiveState('PICKING');
|
||||
|
||||
Reference in New Issue
Block a user