Sync BIM controls with live Launcher session

This commit is contained in:
CODEX 2026-06-23 18:32:04 +03:00
parent ffa60e4a22
commit 168b535953
2 changed files with 95 additions and 2 deletions

View File

@ -186,6 +186,8 @@ const COMMENTS_API = "/api/comments";
const UPLOAD_VERSIONS_API = "/api/uploads/versions";
const SHARE_AUTH_GUEST_PARAM = "ndc_bim_guest";
const SHARE_PROMOTION_TIMEOUT_MS = 4500;
const SHARE_PROMOTION_RETRY_MS = 15000;
const SESSION_STATUS_WATCH_INTERVAL_MS = 2500;
const NODEDC_SESSION_EVENT_TYPE = "nodedc:session:logout";
const acceptByType = {
@ -664,6 +666,8 @@ const isNodeDCSessionLogoutEvent = (value) => (
let nodeDCSessionSyncCleanup = null;
let nodeDCLogoutHandling = false;
let nodeDCSessionWatchTimer = null;
let nodeDCSessionWatchInFlight = false;
const handleNodeDCSessionLogout = async () => {
if (nodeDCLogoutHandling) return;
@ -759,7 +763,8 @@ const scheduleShareSessionPromotion = (shareToken) => {
let storageKey = "";
try {
storageKey = `bimdc-share-promote:${shareToken}`;
if (sessionStorage.getItem(storageKey)) {
const lastAttempt = Number(sessionStorage.getItem(storageKey) || 0);
if (Number.isFinite(lastAttempt) && Date.now() - lastAttempt < SHARE_PROMOTION_RETRY_MS) {
return;
}
sessionStorage.setItem(storageKey, String(Date.now()));
@ -786,6 +791,7 @@ const scheduleShareSessionPromotion = (shareToken) => {
if (payload.type !== "nodedc:bim-share-promoted") return;
done = true;
cleanup();
if (payload.promoted === false) return;
window.location.replace(payload.nextPath || getCurrentReturnPath());
};
@ -798,6 +804,89 @@ const scheduleShareSessionPromotion = (shareToken) => {
document.body.appendChild(iframe);
};
const applyLiveSessionPayload = async (sessionPayload, { reason = "watch" } = {}) => {
const wasAuthenticated = runtimeSessionInfo.authenticated === true;
const isAuthenticated = sessionPayload?.authenticated === true;
runtimeSessionInfo = {
...runtimeSessionInfo,
...(sessionPayload || {}),
authenticated: isAuthenticated,
canShare: sessionPayload?.canShare === true,
user: sessionPayload?.user || null,
};
if (!isAuthenticated) {
if (wasAuthenticated) {
await handleNodeDCSessionLogout();
return;
}
const shareToken = getShareTokenFromPath();
if (shareToken && viewerMode === "guest") {
scheduleShareSessionPromotion(shareToken);
}
return;
}
if (getShareTokenFromPath() && viewerMode === "guest") {
viewerMode = "standard";
}
applyRuntimeMode();
updatePanelsVisibility();
if (reason === "focus" || reason === "visibility") {
loadProjectsList?.().catch(() => {});
}
};
const refreshNodeDCSessionStatus = async (options = {}) => {
if (nodeDCSessionWatchInFlight || nodeDCLogoutHandling) return;
nodeDCSessionWatchInFlight = true;
try {
const response = await fetch(AUTH_SESSION_API, {
credentials: "include",
cache: "no-store",
headers: { "Accept": "application/json" },
});
if (response.ok) {
await applyLiveSessionPayload(await response.json(), options);
}
} catch (_) {
// A transient network failure must not log the user out client-side.
} finally {
nodeDCSessionWatchInFlight = false;
}
};
const setupNodeDCSessionWatcher = () => {
if (nodeDCSessionWatchTimer || typeof window === "undefined") return;
nodeDCSessionWatchTimer = window.setInterval(() => {
void refreshNodeDCSessionStatus({ reason: "interval" });
}, SESSION_STATUS_WATCH_INTERVAL_MS);
window.addEventListener("focus", () => {
void refreshNodeDCSessionStatus({ reason: "focus" });
const shareToken = getShareTokenFromPath();
if (shareToken && viewerMode === "guest") {
scheduleShareSessionPromotion(shareToken);
}
});
window.addEventListener("pageshow", () => {
void refreshNodeDCSessionStatus({ reason: "pageshow" });
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState !== "visible") return;
void refreshNodeDCSessionStatus({ reason: "visibility" });
const shareToken = getShareTokenFromPath();
if (shareToken && viewerMode === "guest") {
scheduleShareSessionPromotion(shareToken);
}
});
};
const COMMENT_TARGET_ICON = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.5 4.75h11A3.25 3.25 0 0 1 20.75 8v6.25a3.25 3.25 0 0 1-3.25 3.25h-5.26l-5.29 3.82V17.5H6.5a3.25 3.25 0 0 1-3.25-3.25V8A3.25 3.25 0 0 1 6.5 4.75Z"></path>
@ -5309,6 +5398,7 @@ const applyViewerState = (state) => {
const initViewer = async () => {
await initializeRuntimeMode();
setupNodeDCSessionSync();
setupNodeDCSessionWatcher();
const preloadHidden = document.getElementById("preload-hidden");
if (preloadHidden) {

View File

@ -929,9 +929,10 @@ const buildSharePromotionUrls = (token) => {
const sharePath = `/share/${encodeURIComponent(safeToken)}`;
const guestPath = `${sharePath}?${SHARE_AUTH_GUEST_PARAM}=1`;
const bridgePath = `/auth/share-promoted?next=${encodeURIComponent(sharePath)}`;
const guestBridgePath = `/auth/share-promoted?next=${encodeURIComponent(sharePath)}&status=guest`;
return {
promoteUrl: buildLauncherLaunchUrl(sharePath),
silentPromoteUrl: buildLauncherLaunchUrl(bridgePath),
silentPromoteUrl: buildLauncherOptionalLaunchUrl(bridgePath, guestBridgePath),
optionalPromoteUrl: buildLauncherOptionalLaunchUrl(sharePath, guestPath)
};
};
@ -1781,6 +1782,7 @@ const handleLauncherHandoff = async (req, res, url) => {
const handleSharePromoted = (res, url) => {
const nextPath = sanitizeReturnTo(url.searchParams.get("next") || "/");
const nextPathJson = JSON.stringify(nextPath);
const promoted = url.searchParams.get("status") !== "guest";
res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Cache-Control", "no-store");
@ -1797,6 +1799,7 @@ const handleSharePromoted = (res, url) => {
const payload = {
type: "nodedc:bim-share-promoted",
nextPath,
promoted: ${promoted ? "true" : "false"},
createdAt: Date.now()
};
if (window.parent && window.parent !== window) {