Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions minify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Build minified copies of uproot's own scripts. uproot serves X.min.js instead
# of X.js only while the SHA-256 in its first line matches X.js (see minified()
# in src/uproot/pages.py). Otherwise, it serves X.js.

set -euo pipefail

cd "$(dirname "$0")/src/uproot/_static"

source="uproot.js"

build="${source%.js}.min.js"
sha256="$(python3 -c 'import hashlib, sys; print(hashlib.file_digest(open(sys.argv[1], "rb"), "sha256").hexdigest())' "$source")"

{
echo "/* Minified from source with SHA-256 $sha256 */"
npx --yes terser@5 "$source" --compress --mangle --ecma 2020
} > "$build.tmp"

mv "$build.tmp" "$build"
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies = [
"python-multipart>=0.0.32,<0.1",
"setproctitle>=1.3.7,<2",
"sortedcontainers>=2.4.0,<3",
"starlette-compress>=1.8.0,<2",
"uvicorn>=0.49.0,<1",
"watchdog>=6.0.0,<7",
"websockets>=16.0,<18",
Expand Down
3 changes: 3 additions & 0 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ set -euxo pipefail
# Sync dependencies
uv sync --extra dev --upgrade

# Build minified scripts
./minify.sh

# Run formatters and autofixes
uv run ruff check --fix src/uproot/
uv run black src/uproot/
Expand Down
34 changes: 1 addition & 33 deletions src/uproot/_static/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions src/uproot/_static/uproot.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ window.uproot = {
dirty: false,
form: null,
futStore: {},
futureCounter: 0,
I: (id_) => document.getElementById(id_),
isInitialized: false,
keepAliveInterval: null,
keepAliveSeconds: null,
key: null,
lastTimeoutLevel: null,
missing: new Set(),
Expand Down Expand Up @@ -301,7 +303,7 @@ window.uproot = {

api(endpoint, data = null) {
return new Promise((resolve, reject) => {
const futid = this.uuid();
const futid = ++this.futureCounter; // short ids save bandwidth
this.futStore[futid] = { resolve, reject };

const message = JSON.stringify({
Expand Down Expand Up @@ -479,12 +481,19 @@ window.uproot = {
},

wsstart() {
if (!(this.keepAliveSeconds > 0)) {
throw new Error("uproot.keepAliveSeconds must be set before wsstart()");
}

this.ws = new RobustWebSocket(this.wsurl(), {
onOpen: (ws) => {
if (this.keepAliveInterval !== null) {
window.clearInterval(this.keepAliveInterval);
}
this.keepAliveInterval = window.setInterval(this.hello, 9000);
this.keepAliveInterval = window.setInterval(
this.hello,
1000 * this.keepAliveSeconds,
);

if (this.isInitialized) {
this.replaySubscriptions();
Expand Down
2 changes: 2 additions & 0 deletions src/uproot/_static/uproot.min.js

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions src/uproot/default/Bare.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,32 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow, noarchive">

{% if not (disable_bootstrap | default(false)) %}
{% if not disable_bootstrap %}
<link rel="stylesheet" href="{{ internalstatic('vendor/bootstrap/bootstrap.min.css') }}">
<script src="{{ internalstatic('vendor/bootstrap/bootstrap.bundle.min.js') }}"></script>
{% endif %}

<link rel="stylesheet" href="{{ internalstatic('style.css') }}">

{% if not (disable_uproot_fonts | default(false)) %}
{% if not disable_uproot_fonts %}
<link rel="stylesheet" href="{{ internalstatic('webfonts.css') }}">
{% if not (disable_tabular_numbers | default(false)) %}
{% if not disable_tabular_numbers %}
<link rel="stylesheet" href="{{ internalstatic('inter-tnum.css') }}">
{% endif %}
{% endif %}

<script src="{{ internalstatic('uproot.js') }}"></script>
{% if not (disable_terms | default(false)) %}
{% if not disable_terms %}
<script src="{{ uproot_terms_url }}"></script>
{% endif %}
<script>
uproot.vars = {{ _uproot_js | tojson | safe }};
window.C = uproot.vars._uproot_internal?.C;
uproot.keepAliveSeconds = uproot.vars._uproot_internal.keepalive_interval;

{% if not (disable_auto_start | default(false)) %}
{% if not disable_auto_start %}
uproot.onStart(() => {
{% if not (disable_connection_lost_modal | default(false)) %}
{% if not disable_connection_lost_modal %}
// Enable Connection lost modal monitoring first (includes startup failsafe)
uproot.enableConnectionLostModal();
{% endif %}
Expand All @@ -57,7 +58,7 @@
<script src="{{ internalstatic('simulate.js') }}"></script>
{% endif %}

{% if not (disable_alpinejs | default(false)) %}
{% if not disable_alpinejs %}
<script src="{{ internalstatic('vendor/alpinejs/cdn.min.js') }}" defer></script>
{% endif %}

Expand Down Expand Up @@ -140,7 +141,7 @@ <h5 class="modal-title">
</div>
</div>

{% if not (disable_connection_lost_modal | default(false)) %}
{% if not disable_connection_lost_modal %}
<div class="modal fade" id="connection-timeout-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false" aria-modal="true" role="alertdialog">
<div class="modal-dialog">
<div class="modal-content">
Expand Down Expand Up @@ -182,6 +183,7 @@ <h5 class="modal-title">
title="{% translate %}Open Chat with Research Coordinator{% endtranslate %}">
<img
src="{{ internalstatic('buddy.svg') }}"
loading="lazy"
width="142"
height="142"
alt="">
Expand Down
2 changes: 1 addition & 1 deletion src/uproot/default/Base.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h1 id="uproot-title" class="my-4">

{% block uproot_main_buttons %}

{% if (buttons | default(true)) and not buttons_placed.placed %}
{% if buttons and not buttons_placed.placed %}
<div id="uproot-button-wrapper">
{% if page.allow_back %}
<button type="button" id="uproot-button-back" class="btn btn-outline-secondary" onclick="uproot.goBack()">
Expand Down
1 change: 1 addition & 0 deletions src/uproot/default/Boilerplate.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<meta charset="utf-8">
<meta name="generator" content="uproot">
<link rel="icon" href="{{ internalstatic('favicon.svg') }}" type="image/svg+xml">

{% block TOPLEVELhead %}
{% endblock TOPLEVELhead %}
Expand Down
1 change: 1 addition & 0 deletions src/uproot/default/admin/Admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<script src="{{ uproot_terms_url }}"></script>
<script>
uproot.vars = {{ _uproot_js | tojson | safe }};
uproot.keepAliveSeconds = uproot.vars.keepalive_interval;

uproot.wsurl = () => {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
Expand Down
21 changes: 21 additions & 0 deletions src/uproot/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
DEFAULT_ROOMS: list["RoomType"] = []
HERE_TOLERANCE: float = 5.0
HOST: str = "127.0.0.1"
# Seconds between the browser's keepalive messages. A lost connection shows
# within this interval plus three seconds. Longer intervals save bandwidth but
# raise shorter dropout tolerances to this interval plus three seconds. At most
# 27 seconds, so that the default dropout tolerance of 30 seconds holds.
# Values near the maximum make dropout detection sensitive to a single late
# keepalive.
KEEPALIVE_INTERVAL: float = 9.0
LANGUAGE: ISO639 = "en"
LOGIN_TOKEN: str | None = None
LOGGER: Any = logging.getLogger("uproot")
Expand Down Expand Up @@ -64,6 +71,20 @@
PUBLIC_DEMO: bool = False
QUICK_ROOM: str | None = None
TBLEXTRA: str = os.getenv("UPROOT_TBLEXTRA", "")
# Project-wide defaults of the switches in participant-facing templates. A
# template overrides them with, e.g., {% set disable_uproot_fonts = True %}.
# Change values in place, e.g., TEMPLATE_DEFAULTS["disable_alpinejs"] = True.
TEMPLATE_DEFAULTS: dict[str, bool] = {
"buttons": True,
"disable_alpinejs": False,
"disable_auto_start": False,
"disable_bootstrap": False,
"disable_connection_lost_modal": False,
"disable_tabular_numbers": False,
"disable_terms": False,
"disable_uproot_fonts": False,
}
TEMPLATE_SWITCHES: frozenset[str] = frozenset(TEMPLATE_DEFAULTS)
TIMEOUT_TOLERANCE: float = 1.0
UNAVAILABLE_EQUIVALENT: str = "null"
UNSAFE: bool = False
Expand Down
16 changes: 15 additions & 1 deletion src/uproot/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# Use in code: i18n.lookup("Welcome", "de") -> "Willkommen"
"""

import hashlib
import os
import re
from collections.abc import Callable
Expand All @@ -34,6 +35,7 @@
MISSING: set[tuple[str, ISO639]] = set()
TERMS: dict[str, dict[ISO639, str]] = {}
JSON: dict[ISO639, str] = {}
VERSIONS: dict[ISO639, str] = {}
VERSION: int = 0


Expand Down Expand Up @@ -113,11 +115,22 @@ def json(target: ISO639) -> str:
if target in translations
}

JSON[target] = orjson.dumps(translations).decode()
JSON[target] = orjson.dumps(translations, option=orjson.OPT_SORT_KEYS).decode()

return JSON[target]


def script(target: ISO639) -> str:
return f"window.uproot = window.uproot || {{}};\nwindow.uproot.terms = {json(target)};\n"


def version(target: ISO639) -> str:
if target not in VERSIONS:
VERSIONS[target] = hashlib.sha256(script(target).encode()).hexdigest()[:16]

return VERSIONS[target]


def load(yaml_path: str) -> None:
"""
Load translation terms from YAML files or directory.
Expand Down Expand Up @@ -173,6 +186,7 @@ def load(yaml_path: str) -> None:

LANGUAGES.update(all_translations.keys())
JSON.clear()
VERSIONS.clear()
VERSION += 1


Expand Down
8 changes: 6 additions & 2 deletions src/uproot/i18ncheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,18 @@ def loosely(key: str) -> str:

def collect_files(top: str, suffixes: tuple[str, ...]) -> list[str]:
"""Return files below top with the given suffixes, skipping hidden files,
hidden directories (such as .venv), and third-party code."""
hidden directories (such as .venv), third-party code, and minified builds."""
result = []
for root, directories, files in os.walk(top):
directories[:] = [
d for d in directories if not d.startswith(".") and d not in SKIPPED_DIRS
]
for f in files:
if f.endswith(suffixes) and not f.startswith("."):
if (
f.endswith(suffixes)
and not f.startswith(".")
and not f.endswith(".min.js")
):
result.append(os.path.join(root, f))
return sorted(result)

Expand Down
30 changes: 29 additions & 1 deletion src/uproot/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later

import asyncio
import functools
from collections.abc import Coroutine
from typing import Any, cast
from uuid import UUID
Expand Down Expand Up @@ -100,6 +101,31 @@ def spawn(coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
return task


DEFAULT_DROPOUT_TOLERANCE = 30.0
KEEPALIVE_SLACK = 3.0 # time allowed for the server's response to a keepalive


def effective_tolerance(tolerance: float) -> float:
"""Browsers only report in every KEEPALIVE_INTERVAL seconds (and responses
may take up to three more), so shorter tolerances would flag everyone."""
minimum = d.KEEPALIVE_INTERVAL + KEEPALIVE_SLACK

if tolerance < minimum:
warn_short_tolerance(tolerance, minimum)

return minimum
else:
return tolerance


@functools.lru_cache(maxsize=64)
def warn_short_tolerance(tolerance: float, minimum: float) -> None:
d.LOGGER.warning(
f"Dropout tolerance of {tolerance} s is below KEEPALIVE_INTERVAL + 3 s, "
f"using {minimum} s instead"
)


async def dropout_watcher(app: FastAPI, interval: float = 3.0) -> None:
removals = set()

Expand All @@ -111,7 +137,9 @@ async def dropout_watcher(app: FastAPI, interval: float = 3.0) -> None:
triplet = [tolerance, fmodule, fname]
last = u.find_online_delta(pid)

if pid not in u.MANUAL_DROPOUTS and (last is None or last <= tolerance):
if pid not in u.MANUAL_DROPOUTS and (
last is None or last <= effective_tolerance(tolerance)
):
# player is online or assumed to be
pass
else:
Expand Down
Loading
Loading