@qtoggle/qui 1.20.0-alpha.2 → 1.20.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qtoggle/qui",
3
3
  "description": "A JavaScript UI library with batteries included.",
4
- "version": "1.20.0-alpha.2",
4
+ "version": "1.20.0-alpha.3",
5
5
  "author": {
6
6
  "name": "Calin Crisan",
7
7
  "email": "ccrisan@gmail.com"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qui-server"
3
- version = "1.20.0-alpha.2"
3
+ version = "1.20.0-alpha.3"
4
4
  description = "A fully fledged qToggle implementation written in Python"
5
5
  authors = [
6
6
  {name = "Calin Crisan", email = "ccrisan@gmail.com"},
package/qui/__init__.py CHANGED
@@ -5,7 +5,7 @@ import secrets
5
5
 
6
6
  from typing import Any
7
7
 
8
- from . import settings
8
+ from . import constants, settings
9
9
 
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -89,7 +89,11 @@ def configure(
89
89
  if not settings.package_name:
90
90
  settings.package_name = re.sub(r"[^a-zA-Z_0-9]", "", settings.name)
91
91
 
92
- if settings.debug:
92
+ if settings.debug or constants.NON_RELEASE_VERSION_RE.match(version):
93
+ # A long-lived cache on static assets is only safe because the build hash changes whenever the content does.
94
+ # Deriving it from a version the release pipeline never stamped would make it one constant shared by every
95
+ # build from source, freezing whatever a browser happened to fetch first. Fall back to a per-process hash, so
96
+ # that restarting the server is enough to invalidate it.
93
97
  settings.build_hash = secrets.token_hex()[:16]
94
98
 
95
99
  else:
package/qui/constants.py CHANGED
@@ -1 +1,9 @@
1
+ import re
2
+
3
+
1
4
  BASE_PREFIX_HEADER = "X-Forwarded-Path"
5
+
6
+ # Matches a version the release pipeline never stamped: an all-zero version, or the literal placeholder word. Spelled
7
+ # as a pattern rather than as plain strings on purpose -- the release tooling rewrites those exact literals wherever
8
+ # they appear in the source, which would quietly turn this into a test against the real version.
9
+ NON_RELEASE_VERSION_RE = re.compile(r"^(?:0(?:\.0)+|unknown[-_]version)$")
@@ -36,8 +36,8 @@
36
36
  background_color|urlquote }}&description={{
37
37
  description|urlquote }}&h={{ build_hash }}">
38
38
  {% endif %}
39
- <link rel="icon" href="{{ static_url }}/img/launcher-icon-32.png">
40
- <link rel="apple-touch-icon" href="{{ static_url }}/img/launcher-icon-32.png">
39
+ <link rel="icon" href="{{ static_url }}/img/launcher-icon-32.png?h={{ build_hash }}">
40
+ <link rel="apple-touch-icon" href="{{ static_url }}/img/launcher-icon-32.png?h={{ build_hash }}">
41
41
 
42
42
  {% block _meta %}
43
43
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
@@ -103,6 +103,27 @@ class JSModuleMapperStaticFileHandler(StaticFileHandler):
103
103
  return False
104
104
 
105
105
 
106
+ class CachedStaticFileHandler(StaticFileHandler):
107
+ # A year, rather than Tornado's ten. Long enough that nothing revalidates in practice, short enough to bound the
108
+ # damage should a build ever be shipped under a build hash that has already been served.
109
+ CACHE_MAX_AGE = 365 * 24 * 3600
110
+
111
+ def get_cache_time(self, path: str, modified: Any, mime_type: str) -> int:
112
+ # Tornado grants its long max-age only to requests carrying a "v" argument, while QUI stamps every asset URL
113
+ # with "h" instead, so by default nothing here is ever cacheable. Honour "h", but only when it matches the hash
114
+ # this process is currently serving: a URL left over from an earlier build names content we can no longer
115
+ # vouch for, and must keep revalidating.
116
+ if self.get_argument("h", None) == settings.build_hash:
117
+ return self.CACHE_MAX_AGE
118
+
119
+ return super().get_cache_time(path, modified, mime_type)
120
+
121
+ def set_extra_headers(self, path: str) -> None:
122
+ # The URL names an exact build, so there is nothing to revalidate even when the user reloads the page.
123
+ if self.get_argument("h", None) == settings.build_hash:
124
+ self.set_header("Cache-Control", f"max-age={self.CACHE_MAX_AGE}, immutable")
125
+
126
+
106
127
  class RedirectFrontendHandler(RequestHandler):
107
128
  def get(self) -> None:
108
129
  base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
@@ -192,7 +213,7 @@ def make_routing_table() -> list[URLSpec]:
192
213
  spec_list.append(
193
214
  URLSpec(
194
215
  rf"^{static_url}/(.*)$",
195
- StaticFileHandler,
216
+ CachedStaticFileHandler,
196
217
  {"path": frontend_path},
197
218
  name="static",
198
219
  )