gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit20e09d8920e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre20e09d89/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

  • 20e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre
  • f83571c3tracker#18 (mission 067): daily sync by change lists — TMDB /tv|movie/changes (since the stored day, paged) + TVmaze /updates/shows → only our changed titles (followed: full step, unfollowed: light step — changed seasons, no TVmaze), full walk on first run / gap > 14 days / failed list; show record refreshed (title, tmdbSummary, tagline, status, genres …; renamed titles re-indexed); summary = the creator's own text (page: summary > tmdbSummary > tvmazeSummary), one-time clear of copied summaries (9,647 on the live copy); gate 261, tests/realdata-018*.mjs, README + STATUSmre
  • f2b00674Merge t26 (tracker#26 + #28) into main (mission 062): short ids for every new person (castPersonId, guest route), guest stars stored on the title and created as people only when opened (/person/tmdb/<id>?show=<id> → 302), lean watch/follow clicks (showRow a small object, cast/crew from the slug, watches cached per user, face rows only after a season toggle); gate 311, tests/realdata-062.mjs, README + STATUSmre
  • fa1f9dfatracker#29 (mission 063): unwatched check muted grey outline + check (accent only on hover), watched stays solid — no code regression, the accent outline read as ticked; gate checks real checks visibly (computed style + screenshot pixel) on /my/unwatched, show, movie, /my/movies; gate 266, tests/realdata-063.mjs, README + STATUSmre
  • 10bb3f93tracker#28 (mission 061): full cast (all seasons, main cast by episodes, guest stars) + crew (created by, directed by, written by, screenplay, story, music) — stored by the details completion, the daily sync, the search import (one details request) and a background credits job (resumes, RSS limit); show page collapsed after 20 with client-side Show all; showBySlug via a slug map; gate 249, tests/realdata-028.mjs, README + STATUSmre
  • 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
  • 25a50bc4tracker#26 (mission 059): titles from a filmography are completed — on open (skeleton, step-wise face showComplete, no reload) and by the in-app details repair (resumes, TMDB-paced, series in parts); cast from TMDB credits; gate 231, tests/realdata-026.mjs, 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