fiftyone-app / patch_fiftyone_frontend.py
harpreetsahota's picture
Recover browser sessions after restart
2c24d86 verified
Raw
History Blame Contribute Delete
5.47 kB
"""Patches the packaged FiftyOne frontend for a stateless hosted demo."""
import importlib.util
import json
import logging
from pathlib import Path
import re
ENV_ASSIGNMENT_PATTERN = re.compile(
r"(__vite_import_meta_env__\$\w+=\{)([^}]*)(\})"
)
HEARTBEAT_INTERVAL_MS = 60_000
HEARTBEAT_MARKER = "fiftyone-hf-session-heartbeat"
MANIFEST_PATH = Path("/app/datasets.json")
LOG_FORMAT = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
logger = logging.getLogger(__name__)
def main():
"""Enables stateless state and browser session heartbeats."""
static_dir = _get_static_dir()
dataset_names = _get_dataset_names()
_enable_stateless_frontend(static_dir)
_add_session_heartbeat(static_dir, dataset_names)
def _get_static_dir():
"""Returns the static frontend directory in the installed package."""
spec = importlib.util.find_spec("fiftyone")
if spec is None or spec.origin is None:
raise RuntimeError("Could not locate the installed FiftyOne package")
static_dir = Path(spec.origin).parent / "server" / "static"
if not static_dir.is_dir():
raise RuntimeError(
f"Could not locate FiftyOne static assets at {static_dir}"
)
logger.info("Located FiftyOne frontend at %s", static_dir)
return static_dir
def _get_dataset_names():
"""Gets the configured base dataset names from the manifest."""
with open(MANIFEST_PATH) as file:
manifest = json.load(file)
names = [entry["name"] for entry in manifest["datasets"]]
if not names:
raise ValueError("datasets.json must contain at least one dataset")
return names
def _enable_stateless_frontend(static_dir):
"""Enables URL-local state in every compiled Vite environment object."""
javascript_files = sorted((static_dir / "assets").glob("*.js"))
reference_count = 0
environment_assignments = 0
patched_assignments = 0
def _inject_flag(match):
nonlocal environment_assignments, patched_assignments
environment_assignments += 1
properties = match.group(2)
if "VITE_NO_STATE:" in properties:
return match.group(0)
patched_assignments += 1
return (
match.group(1)
+ "VITE_NO_STATE:!0,"
+ properties
+ match.group(3)
)
for path in javascript_files:
content = path.read_text()
reference_count += content.count(".VITE_NO_STATE")
patched = ENV_ASSIGNMENT_PATTERN.sub(_inject_flag, content)
if patched != content:
path.write_text(patched)
logger.info("Enabled stateless mode in %s", path.name)
if reference_count == 0:
raise RuntimeError(
"The packaged frontend contains no VITE_NO_STATE references"
)
if environment_assignments == 0:
raise RuntimeError("No compiled Vite environment objects were found")
logger.info(
"Stateless patch complete: references=%d environments=%d patched=%d",
reference_count,
environment_assignments,
patched_assignments,
)
def _add_session_heartbeat(static_dir, dataset_names):
"""Adds a heartbeat that keeps the browser's dataset clone alive."""
index_path = static_dir / "index.html"
content = index_path.read_text()
if HEARTBEAT_MARKER in content:
logger.info("Browser session heartbeat is already installed")
return
session_script = _get_session_script(dataset_names)
if "</head>" not in content:
raise RuntimeError(f"Could not patch missing </head> in {index_path}")
index_path.write_text(
content.replace("</head>", session_script + "</head>", 1)
)
logger.info(
"Installed browser heartbeat with interval=%dms",
HEARTBEAT_INTERVAL_MS,
)
def _get_session_script(dataset_names):
"""Returns the inline browser identity and heartbeat script."""
base_paths = json.dumps(
[f"/datasets/{name}" for name in dataset_names],
separators=(",", ":"),
)
return (
f'<script id="{HEARTBEAT_MARKER}">'
f"if({base_paths}."
'includes(location.pathname))location.replace("/");'
'const foSessionKey="fiftyone_demo_session";'
'const foFetch=window.fetch.bind(window);'
'window.fetch=(input,init={})=>{'
'const token=localStorage.getItem(foSessionKey);'
'const target=typeof input==="string"?input:'
'input instanceof Request?input.url:String(input);'
'const url=new URL(target,location.href);'
'if(token&&url.origin===location.origin){'
'const sourceHeaders=init.headers||'
'(input instanceof Request?input.headers:void 0);'
'const headers=new Headers(sourceHeaders);'
'headers.set("X-FiftyOne-Session",token);'
'init={...init,headers};'
'}return foFetch(input,init);'
'};'
'setInterval(()=>{'
'const dataset=location.pathname.split("/").pop();'
'fetch("/__session/heartbeat?dataset="+encodeURIComponent(dataset),'
'{method:"POST",credentials:"same-origin"}).then(response=>{'
'if(response.status===401){'
'localStorage.removeItem(foSessionKey);'
'location.replace("/");'
'}'
'});'
'},'
f"{HEARTBEAT_INTERVAL_MS});"
"</script>"
)
if __name__ == "__main__":
main()