feedkeeper 0.23.1 → 0.24.1

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.
@@ -30,7 +30,7 @@
30
30
  <meta name="twitter:image" content="/logo.svg" />
31
31
  <!-- Inline so the splash paints before any script or stylesheet loads -->
32
32
  <style>html{background:#f8fafc}html[data-theme=dark]{background:#0b0f17}@media (prefers-color-scheme:dark){html:not([data-theme=light]){background:#0b0f17}}#splash{z-index:100;background:inherit;justify-content:center;align-items:center;transition:opacity .3s;display:flex;position:fixed;inset:0}#splash img{width:62px;height:62px;animation:.4s both splash-in}#splash.splash-hidden{opacity:0;pointer-events:none}@keyframes splash-in{0%{opacity:0;transform:scale(.92)}}@media (prefers-reduced-motion:reduce){#splash,#splash img{transition:none;animation:none}}</style>
33
- <script type="module" crossorigin src="/assets/index-xx6TyOwB.js"></script>
33
+ <script type="module" crossorigin src="/assets/index-BjhXf9OJ.js"></script>
34
34
  <link rel="stylesheet" crossorigin href="/assets/index-B4e2oqbo.css">
35
35
  </head>
36
36
  <body>
package/web/dist/sw.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // FeedKeeper service worker. Built into dist/sw.js, where the placeholder below is
2
2
  // replaced with a hash of the build's file names so every release gets its own app cache.
3
- const BUILD_ID = "93f2f6a49c67";
3
+ const BUILD_ID = "3c5196c9a4ba";
4
4
  const STATIC_CACHE = `fk-static-${BUILD_ID}`;
5
5
  const API_CACHE = "fk-api";
6
6
  const SHELL = "/index.html";
@@ -15,7 +15,8 @@ const MAX_CACHED_IMAGES = 3000;
15
15
  // (tokens, users, settings, mutations) always goes to the server.
16
16
  const READABLE_API = [/^\/api\/onboarding\/status$/, /^\/api\/config$/, /^\/api\/auth\/me$/, /^\/api\/feeds$/, /^\/api\/folders$/, /^\/api\/items$/, /^\/api\/filters\/muted$/];
17
17
  const FEED_ICON = /^\/api\/feeds\/\d+\/icon$/;
18
- const IMMUTABLE_API = [/^\/api\/archive\/images\/\d+$/, /^\/api\/items\/\d+\/image$/, /^\/api\/auth\/me\/avatar$/];
18
+ const MUTABLE_IMAGE = /^\/api\/items\/\d+\/image$/;
19
+ const IMMUTABLE_API = [/^\/api\/archive\/images\/\d+$/, /^\/api\/auth\/me\/avatar$/];
19
20
  // Files next to index.html (theme script, manifest, icons) are the only other thing served from the cache. Any other
20
21
  // path answers per request and per signed-in user, so a stored copy could show one account another's data.
21
22
  const STATIC_FILE = /^\/[^/]+\.(?:js|css|svg|png|ico|webmanifest|txt|woff2?)$/;
@@ -56,6 +57,8 @@ self.addEventListener("fetch", (event) => {
56
57
  if (url.searchParams.has("offline")) return;
57
58
  if (FEED_ICON.test(url.pathname)) {
58
59
  event.respondWith(staleWhileRevalidate(API_CACHE, request));
60
+ } else if (MUTABLE_IMAGE.test(url.pathname)) {
61
+ event.respondWith(mutableImage(request));
59
62
  } else if (IMMUTABLE_API.some((pattern) => pattern.test(url.pathname))) {
60
63
  event.respondWith(cacheFirst(API_CACHE, request, MAX_CACHED_IMAGES));
61
64
  } else if (READABLE_API.some((pattern) => pattern.test(url.pathname))) {
@@ -114,6 +117,31 @@ async function cacheFirst(cacheName, request, limit = 0) {
114
117
  return response;
115
118
  }
116
119
 
120
+ async function mutableImage(request) {
121
+ const cache = await caches.open(API_CACHE);
122
+ const cached = await cache.match(request, MATCH);
123
+ const date = cached && Date.parse(cached.headers.get("date") ?? "");
124
+ const maxAge = Number(cached?.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1] ?? 0);
125
+ // Old immutable proxy responses must also expire at the new six-hour boundary.
126
+ if (cached && !cached.headers.get("cache-control")?.includes("no-store") && Date.now() - date < Math.min(maxAge, 21600) * 1000) return cached;
127
+ try {
128
+ const headers = new Headers(request.headers);
129
+ if (cached?.headers.get("etag")) headers.set("If-None-Match", cached.headers.get("etag"));
130
+ let response = await withTimeout(fetch(new Request(request, { headers, cache: "no-cache" })), NETWORK_TIMEOUT_MS);
131
+ if (response.status === 304 && cached) {
132
+ const refreshed = new Headers(cached.headers);
133
+ for (const name of ["date", "etag", "cache-control"]) if (response.headers.has(name)) refreshed.set(name, response.headers.get(name));
134
+ refreshed.set("date", response.headers.get("date") ?? new Date().toUTCString());
135
+ response = new Response(await cached.arrayBuffer(), { status: 200, headers: refreshed });
136
+ }
137
+ if (response.ok) {
138
+ if (response.headers.get("cache-control")?.includes("no-store")) await cache.delete(request);
139
+ else { await cache.put(request, response.clone()); await trim(cache, key => /\/api\/(archive\/images\/|items\/\d+\/image)/.test(key.url), MAX_CACHED_IMAGES); }
140
+ }
141
+ return response; // An online denial must never fall back to the previous account's bytes.
142
+ } catch { return cached ?? Response.error(); }
143
+ }
144
+
117
145
  async function networkFirst(cacheName, request, limit) {
118
146
  const cache = await caches.open(cacheName);
119
147
  try {