gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit139fafd8139fafd8tracker#16: short bio (4 lines, click = all), real-data check script, README + STATUSmre139fafd8/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

  • 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
  • 31b758aatracker#10: installable app (manifest, service worker, offline shell), own icon + faviconmre
  • 2fa9d997tracker#9: TMDB sync (followed shows: seasons, episodes, posters), tools/sync-tmdb.hl + daily run 04:00 UTC, fake TMDB in gatemre
  • 49e1f61edeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
  • 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
  • 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
  • 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
  • a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre