gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitd8b12d67d8b12d67tracker#27 (mission 060): short ids for movies, series and persons — old 702 kept (data/old-short-ids.json), new random [a-z0-9]{5} unique across both, claimed at creation, background backfill (resumes), shown under poster/photo, /<shortId> → 301; gate 259, tests/realdata-060*, README + STATUSmred8b12d67/plugins/web/worker.hl

6.0 KB

  1. // The framework's SERVICE WORKER (COMPONENTS §10, D38) — the offline shell. It exists
  2. // only for an app that declares `offline = [ … ]`: hl:web compiles this file like
  3. // client.hl, serves it at /__hl/web/worker.js, and registers the entry script
  4. // /__hl/sw.js, which imports it and constructs it ONCE with the table below.
  5. //
  6. // WHAT IS KEPT IS THE APP'S DECISION (D38). `precache` holds the shared shell —
  7. // the runtime, the client half, the stylesheet, every page module, the web app
  8. // manifest and its icons — plus the DOCUMENT of each param-free route whose
  9. // component the allow-list names. `routes` is the client's route table with
  10. // `keep` on the rows the allow-list names, so a param-bearing page (`/notes/:id`)
  11. // is kept when it is VISITED, and only then. Every other page is unavailable
  12. // offline, and says so.
  13. //
  14. // THREE RULES, the ones the old hl:web ran (mission 101):
  15. // · a NAVIGATION is network-first. The network is the truth while it answers,
  16. // and a fresh document for a kept page replaces the cached one on the way past;
  17. // the cache is what an outage falls back to.
  18. // · a precached asset is cache-first. Its url carries the build's hash, so a hit
  19. // cannot be stale.
  20. // · everything else goes to the network untouched: the emit channel, a function
  21. // route, an app's own file. The socket never reaches a worker at all.
  22. //
  23. // WHY THE ENTRY SCRIPT IS NOT THIS FILE. A worker must add its listeners while its
  24. // script is first evaluated, and a `fetch` listener must hand `respondWith` its
  25. // promise before the listener returns. Every call in this language is awaited, so
  26. // neither can be written here; /__hl/sw.js does exactly those two things and calls
  27. // the methods below for everything else (WebFramework.hl, `workerEntry`).
  28. import HlwRouter from './router.hl'
  29. cfgText = '' // the table, as JSON text (the one construction argument)
  30. cfg = JSON.parse(cfgText)
  31. router = new HlwRouter(routes = cfg.routes)
  32. // INSTALL: the shell into this build's cache, one url at a time — a single failed
  33. // url must not throw the whole install away and leave the app with no shell
  34. install() {
  35. let cache = globalThis.caches.open(cfg.cache)
  36. for (url of cfg.precache) {
  37. let r = globalThis.__hlAttempt(() => { return fetch(url, { cache = 'reload' credentials = 'same-origin' }) })
  38. if (r.ok && r.value.ok) { cache.put(url, r.value) }
  39. }
  40. globalThis.skipWaiting()
  41. return null
  42. }
  43. // ACTIVATE: the cache name carries the build's hash, so every other hl cache is an
  44. // older deploy's. This worker then controls the pages that are already open.
  45. activate() {
  46. for (name of globalThis.caches.keys()) {
  47. if (name != cfg.cache && name.startsWith('hl-shell-')) { globalThis.caches.delete(name) }
  48. }
  49. globalThis.clients.claim()
  50. return null
  51. }
  52. // FETCH: what answers this request. Never null — the entry script hands the answer to
  53. // `respondWith` unread.
  54. answer(req) {
  55. if (req.method != 'GET') { return fetch(req) }
  56. let url = new URL(req.url)
  57. if (url.origin != globalThis.location.origin) { return fetch(req) }
  58. if (req.mode == 'navigate') { return navigation(req, url) }
  59. if (cfg.precache.includes(url.pathname + url.search)) { return cacheFirst(req, url.pathname + url.search) }
  60. return fetch(req)
  61. }
  62. // may the document at this path be kept? Only if the route that answers it is a page
  63. // the allow-list names — the whole of D38 in one predicate
  64. kept(path) {
  65. let m = router.match(path)
  66. return m != null && m.route.keep == true
  67. }
  68. navigation(req, url) {
  69. let r = globalThis.__hlAttempt(() => { return fetch(req) })
  70. if (r.ok) {
  71. if (r.value.ok && kept(url.pathname)) { keep(url.pathname + url.search, r.value.clone()) }
  72. return r.value
  73. }
  74. let hit = globalThis.caches.match(url.pathname + url.search, { cacheName = cfg.cache })
  75. if (hit != null) { return hit }
  76. return unavailable(url.pathname)
  77. }
  78. keep(key, res) {
  79. let cache = globalThis.caches.open(cfg.cache)
  80. cache.put(key, res)
  81. return null
  82. }
  83. // CACHE-FIRST for the shell. A miss still asks the network, so a worker installed
  84. // before an asset existed is not a permanent failure.
  85. cacheFirst(req, key) {
  86. let hit = globalThis.caches.match(key, { cacheName = cfg.cache })
  87. if (hit != null) { return hit }
  88. let r = globalThis.__hlAttempt(() => { return fetch(req) })
  89. if (r.ok) {
  90. if (r.value.ok) { keep(key, r.value.clone()) }
  91. return r.value
  92. }
  93. let R = globalThis.Response
  94. return new R('', { status = 503 statusText = 'Offline' })
  95. }
  96. // A MESSAGE FROM A PAGE. `keep`: a page the browser reached by a client-side
  97. // navigation was never a document request, so the client asks for it to be kept —
  98. // fetched here as the document it would be, if it is a page the allow-list names.
  99. message(data) {
  100. if (data == null || data.t != 'keep' || !kept(data.path)) { return null }
  101. let r = globalThis.__hlAttempt(() => { return fetch(data.path, { credentials = 'same-origin' headers = { 'Accept' = 'text/html' } }) })
  102. if (r.ok && r.value.ok) { keep(data.path, r.value) }
  103. return null
  104. }
  105. // A NOTIFICATION WAS CLICKED: the app's open window is focused, or one is opened at
  106. // the path the notification carries
  107. clicked(n) {
  108. n.close()
  109. let path = n.data != null && n.data.path != null ? n.data.path : '/'
  110. for (c of globalThis.clients.matchAll({ type = 'window' includeUncontrolled = true })) {
  111. if (c.focus != null) { return c.focus() }
  112. }
  113. return globalThis.clients.openWindow(path)
  114. }
  115. // A page this app does not carry offline. A DOCUMENT, so the tab shows something and
  116. // no navigation fails inside the browser; 503, so a probe can tell it from a page.
  117. unavailable(path) {
  118. let esc = path.replaceAll('&', '&amp;').replaceAll('<', '&lt;')
  119. let body = '<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Offline</title><link rel="stylesheet" href="' + cfg.css + '"></head><body><main data-hl-offline="1"><h1>Unavailable offline</h1><p><code>' + esc + '</code> is not one of the pages this app carries offline.</p><p><a href="/">Back to the start</a></p></main></body></html>'
  120. let R = globalThis.Response
  121. return new R(body, { status = 503 headers = { 'Content-Type' = 'text/html; charset=utf-8' } })
  122. }

Branches

Latest commits

  • d8b12d67tracker#27 (mission 060): short ids for movies, series and persons — old 702 kept (data/old-short-ids.json), new random [a-z0-9]{5} unique across both, claimed at creation, background backfill (resumes), shown under poster/photo, /<shortId> → 301; gate 259, tests/realdata-060*, README + STATUSmre
  • f14db671tracker#22-#25 (mission 058): episode air dates, season check = all episodes watched, movie watched check (+ /my/movies count), /genres/<genre> pages (movies + series, newest first, paginated); gate 238, tests/realdata-058.mjs, README + STATUSmre
  • 1704ec45tracker#17 (mission 057): season caret down/up, skeleton rows while a season loads, sessionless showSeasonEpisodes face (no page re-mount), client-only close; gate 214, tests/realdata-057.mjs, README + STATUSmre
  • f2fe3e36mission 056: README + STATUS (merge, fixes, Hybriel 8590df63, real-data check), tests/realdata-056.mjs, tools/check-public-slugs.hlmre
  • f40c250emission 056: re-vendor hybriel master 8590df63 (#121, #122); an adult title's page is Not found for non-followers; gate: leave the page before stopping the servermre
  • 2b7fdd6cmission 056: signed-out header one row on phones ("Log in", nowrap), backfill skips adult titles' posters, gate checksmre
  • 2c53d5efMerge branch 't16-person' (tracker#16 person pages) into main; filmography shows only public titles (054 adult flag), gate race fix (backfill start line)mre
  • c171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmre
  • 139fafd8tracker#16: short bio (4 lines, click = all), real-data check script, README + STATUSmre
  • 93be9476tracker#16: person pages /person/<slug> with the filmography fetched from TMDB on the first visit (step by step), gatemre
  • 47a3cae6STATUS: mission 053 merge commit idsmre
  • dcc5eecaMerge branch 't14-search'mre
  • 03edc783Merge branch 't15-tvmaze'mre
  • 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre
  • 6bb2daf1tracker#13: homepage (tiles, intro, latest movies/shows), /shows, /movies/page/N, /my/movies; lists cached in memorymre
  • b8bd1157tracker#14: README + STATUS (search, real-data numbers, gate, merge notes)mre
  • 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
  • 34f2c15btracker#15: TVmaze merge in the sync (gaps only: new episodes/seasons, empty titles/air dates; numbering check), fake TVmaze episodes + gatemre
  • cbdc4ea7tracker#12: link icons TMDB/IMDb/TVDB/TVmaze; sync fills missing ids (TVmaze lookup); movies fetched via /movie/mre
  • b105bcd8tracker#11: Hybriel master ff51cf46 (checks no longer vanish), mobile-first styles, carets, follow button, sign-in modal, inverted check, orange castmre