Remove non-TLS support
This commit is contained in:
1 parent
84c07a912b
commit
b59ef914da
9 files changed
+88
-171
No files matched your search
+22
-57
@@ -19,13 +19,22 @@
|
||||
The pages on disk are fragments and the shell is a template, and it is the
|
||||
worker that puts them together. So this page has one job: register the
|
||||
worker and reload once it is running, which lands on the real app at the
|
||||
address that was asked for. It is only ever seen once per device. -->
|
||||
address that was asked for. It is only ever seen once per device.
|
||||
|
||||
There is no fallback for a device that cannot install one. There used to
|
||||
be -- this page loaded the renderer itself and composed the page in the
|
||||
browser -- and it existed for one case: the app served over plain http://
|
||||
from a LAN address, which is not a secure context, so no worker can
|
||||
register. This app is served over TLS, where that case cannot arise, and
|
||||
the fallback was a second way of building every page that nothing
|
||||
exercised. What is left when registration fails is a sentence saying so,
|
||||
because there is nothing honest to show instead. -->
|
||||
|
||||
<main class="boot container py-5 text-center">
|
||||
<div class="spinner-border text-secondary mb-3" role="status" aria-hidden="true"></div>
|
||||
<p id="boot-status" class="text-body-secondary">Preparing the offline copy…</p>
|
||||
<noscript><p class="alert alert-warning d-inline-block">This app builds its
|
||||
pages in the browser and needs JavaScript.</p></noscript>
|
||||
pages on the device and needs JavaScript.</p></noscript>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -50,7 +59,7 @@
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (tries >= 2) return compose('The offline copy could not start.');
|
||||
if (tries >= 2) return fail('The offline copy could not start.');
|
||||
try { sessionStorage.setItem('boot:tries', String(tries + 1)); } catch (err) {}
|
||||
|
||||
navigator.serviceWorker.register('sw.js').then(function (registration) {
|
||||
@@ -58,70 +67,26 @@
|
||||
// controlled yet. Otherwise wait for the one installing to get there.
|
||||
if (registration.active) return go();
|
||||
var worker = registration.installing || registration.waiting;
|
||||
if (!worker) return compose('The offline copy did not install.');
|
||||
if (!worker) return fail('The offline copy did not install.');
|
||||
worker.addEventListener('statechange', function (event) {
|
||||
if (event.target.state === 'activated') go();
|
||||
});
|
||||
}, function (err) {
|
||||
compose('Could not install the offline copy: ' + err.message);
|
||||
fail('Could not install the offline copy: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Everything below is the path where there will never be a worker: a browser
|
||||
// without them, or -- much more likely -- the app served over plain http://
|
||||
// from a LAN address, which is not a secure context. Rather than spin here
|
||||
// forever, the page composes itself, using the same renderer and the same
|
||||
// templates the worker would have used. No offline copy and no bookmark, but
|
||||
// the text reads. Delete this if the app is only ever served over TLS.
|
||||
function compose(why) {
|
||||
console.warn(why);
|
||||
status('Loading…');
|
||||
script('js/nunjucks.min.js')
|
||||
.then(function () { return script('js/render.js'); })
|
||||
.then(function () {
|
||||
var view = createRenderer(readText);
|
||||
var page = location.pathname.split('/').pop();
|
||||
return readText('./' + page + location.search)
|
||||
.catch(function () {
|
||||
return view.notice('Not available',
|
||||
'This page could not be loaded. Check your connection and try again.');
|
||||
})
|
||||
// mark is null: the bookmark lives in the worker's IndexedDB, and
|
||||
// there is no worker to ask. bookmark.njk renders the slot hidden
|
||||
// and it fills itself the moment one appears.
|
||||
.then(function (fragment) { return view.compose(fragment, null); });
|
||||
})
|
||||
.then(become, function (err) { status('Could not start: ' + err.message); });
|
||||
}
|
||||
|
||||
function script(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var el = document.createElement('script');
|
||||
el.src = src;
|
||||
el.onload = function () { resolve(); };
|
||||
el.onerror = function () { reject(new Error('cannot load ' + src)); };
|
||||
document.head.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function readText(path) {
|
||||
return fetch(path, { credentials: 'same-origin' }).then(function (res) {
|
||||
if (!res.ok) throw new Error(path + ': ' + res.status);
|
||||
return res.text();
|
||||
});
|
||||
}
|
||||
|
||||
// Replaces this document in place: same URL, same history entry, no second
|
||||
// request. Written rather than assigned into the DOM because the shell
|
||||
// carries <script> tags, and scripts put into a page any other way never run.
|
||||
function become(html) {
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
// The end of the road. Every page in this app is put together by the
|
||||
// worker, so a device that cannot install one has nothing to read here --
|
||||
// and a spinner that turns forever says less than the reason does.
|
||||
function fail(why) {
|
||||
var spinner = document.querySelector('.boot .spinner-border');
|
||||
if (spinner) spinner.remove();
|
||||
status(why);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) start();
|
||||
else compose('This browser has no service worker support.');
|
||||
else fail('This browser has no service worker support.');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+10
-27
@@ -1,7 +1,7 @@
|
||||
// Page startup.
|
||||
//
|
||||
// Pages arrive whole -- composed by the service worker, or by boot.html on the
|
||||
// visit before there is one -- so there is nothing here that builds a page.
|
||||
// Pages arrive whole -- the service worker composes every one of them -- so
|
||||
// there is nothing here that builds a page.
|
||||
// What is left is the three things a composed document cannot do for itself:
|
||||
// put a swapped-in page where it belongs before it is painted, land the
|
||||
// reference picker where the reader asked to go, and tell the chrome when a
|
||||
@@ -94,11 +94,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// A worker-composed page is an ordinary document and the browser scrolls to
|
||||
// the anchor itself. One composed by boot.html is not: it replaced a
|
||||
// document the browser had already finished with, so the fragment identifier
|
||||
// has been spent. Repeating it costs nothing in the first case and is what
|
||||
// makes a link to GEN#v1_1 land on the verse in the second.
|
||||
// The browser scrolls to the anchor itself on a document it navigated to,
|
||||
// so this is for the one it does not: a reload that landed on boot.html and
|
||||
// was reloaded again into the real page. Costs nothing when the browser has
|
||||
// already been there.
|
||||
function scrollToHash() {
|
||||
var el = hashTarget();
|
||||
if (el) jumpTo(el);
|
||||
@@ -166,25 +165,12 @@
|
||||
//
|
||||
// Toggling the class rather than the style because Bootstrap declares
|
||||
// .d-none !important, which an inline display would lose to.
|
||||
//
|
||||
// Only the digest line is missing on a page boot.html composed -- there was
|
||||
// no worker to name a version -- so the element itself is always here.
|
||||
function showVersion() {
|
||||
var el = document.getElementById('app-version');
|
||||
if (!el) return;
|
||||
el.classList.toggle('d-none', !document.querySelector('#content .index'));
|
||||
}
|
||||
|
||||
// api/bookmark has no origin server -- it is answered by the worker -- so
|
||||
// there is nothing to ask until the worker is controlling this page.
|
||||
// controller, not ready: ready can resolve a tick before clients.claim()
|
||||
// takes effect, and a request sent in that gap reaches the network and 404s.
|
||||
function whenControlled(fn) {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
if (navigator.serviceWorker.controller) return fn();
|
||||
navigator.serviceWorker.addEventListener('controllerchange', fn, { once: true });
|
||||
}
|
||||
|
||||
// Deferred scripts run before DOMContentLoaded fires, so this always lands,
|
||||
// and it lands after htmx has processed the shell.
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
@@ -192,15 +178,12 @@
|
||||
showVersion();
|
||||
|
||||
// Registered from the page it is going to serve, not from a page of its
|
||||
// own: there is nothing to wait for and nothing to reload. This load is
|
||||
// already the real app -- the worker installs alongside it, and takes over
|
||||
// composing, the bookmark and the offline download from the next
|
||||
// navigation on.
|
||||
// own: this load is already the real app, composed by the worker that is
|
||||
// controlling it. What this catches is the copy on disk having changed
|
||||
// since -- the browser refetches sw.js, sees different bytes, and installs
|
||||
// the new one alongside, which takes over from the next navigation on.
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('sw.js').catch(function () {});
|
||||
}
|
||||
whenControlled(function () {
|
||||
document.body.dispatchEvent(new CustomEvent('worker-ready'));
|
||||
});
|
||||
});
|
||||
})();
|
||||
+16
-19
@@ -1,17 +1,15 @@
|
||||
// The renderer, shared by the two things that ever build a page: the service
|
||||
// worker, which composes every navigation once it is installed, and boot.html,
|
||||
// which composes the one navigation that arrives before there is a worker to
|
||||
// do it.
|
||||
// The renderer: the templates in templates/, and the two things sw.js does
|
||||
// with them -- compose a fragment into the shell, and stand a notice in for a
|
||||
// page that could not be delivered.
|
||||
//
|
||||
// Sharing it is the whole point. A shell only the worker could build is what
|
||||
// made a first visit show a spinner and reload; with the same templates and
|
||||
// the same code reachable from the page, the first visit is already the app
|
||||
// and the worker is what makes the second one fast and the hundredth one
|
||||
// offline.
|
||||
// A file of its own rather than more of sw.js because it is the half with
|
||||
// nothing to do with the network: no routes, no caches, no requests. It was
|
||||
// loaded by boot.html as well once, which composed pages in the browser for a
|
||||
// device that could not install a worker; nothing but the worker builds a page
|
||||
// now.
|
||||
//
|
||||
// Loaded as a plain script by both -- importScripts in the worker, a <script>
|
||||
// tag in the page -- so it defines a global rather than exporting. Nunjucks
|
||||
// must already be loaded.
|
||||
// Loaded with importScripts, so it defines a global rather than exporting.
|
||||
// Nunjucks must already be loaded.
|
||||
(function (scope) {
|
||||
'use strict';
|
||||
|
||||
@@ -123,10 +121,9 @@
|
||||
//
|
||||
// chrome is what the shell needs and the fragment cannot carry: the
|
||||
// bookmark, the navbar picker's model, and the two versions the worker
|
||||
// was built with. All three are null from boot.html, which has no worker
|
||||
// to ask for any of them; the first two render as a hidden slot that
|
||||
// fills itself if one ever appears, and the third renders as nothing,
|
||||
// because no worker is the whole of what there is to report.
|
||||
// was built with. reference is null on the book list, which is the
|
||||
// picker's hidden state; the other two are always there, because the
|
||||
// worker composing this is the thing that knows them.
|
||||
compose: function (fragment, chrome) {
|
||||
var title = (TITLE_RE.exec(fragment) || ['', ''])[1].trim();
|
||||
var content = fragment.replace(TITLE_RE, '');
|
||||
@@ -134,9 +131,9 @@
|
||||
return render(name, {
|
||||
title: title,
|
||||
content: content,
|
||||
mark: (chrome && chrome.mark) || null,
|
||||
reference: (chrome && chrome.reference) || null,
|
||||
version: (chrome && chrome.version) || null
|
||||
mark: chrome.mark,
|
||||
reference: chrome.reference,
|
||||
version: chrome.version
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
+14
-24
@@ -9,8 +9,7 @@
|
||||
//
|
||||
// GET / the book list
|
||||
// GET /:page one book -- /GEN, /EXO
|
||||
// GET /api/bookmark the navbar slot
|
||||
// POST /api/bookmark set the bookmark, and return the slot
|
||||
// POST /api/bookmark set the bookmark, and return the navbar slot
|
||||
// GET /api/reference the navbar's chapter and verse picker
|
||||
// GET /api/verse the dialog for one verse
|
||||
// GET /api/toast one confirmation, by name
|
||||
@@ -28,11 +27,9 @@
|
||||
// navigation -- one route, both callers, which is the whole reason the request
|
||||
// object carries `navigate`.
|
||||
//
|
||||
// The composing is in js/render.js, which boot.html can load too. Normally it
|
||||
// does not: boot.html registers this worker and reloads into it. It composes in
|
||||
// the page when no worker is possible at all -- no secure context, or a browser
|
||||
// without them -- and then it does so from these same templates rather than
|
||||
// leaving a spinner up forever.
|
||||
// The composing is in js/render.js. Every page in the app comes through it:
|
||||
// boot.html registers this worker and reloads into it, and from there nothing
|
||||
// else ever builds one.
|
||||
//
|
||||
// Every same-origin GET is network-first: ask the network, fall back to the
|
||||
// cache when it fails. Offline, fetch rejects immediately, so the fallback is
|
||||
@@ -461,16 +458,12 @@ router.get('/', (call) => servePage('./', '', call));
|
||||
router.get('/:page', (call) =>
|
||||
servePage('./' + call.params.page, call.params.page, call));
|
||||
|
||||
router.get('/api/bookmark', () => bookmarkFragment(''));
|
||||
|
||||
// The picker, and the two moves it makes.
|
||||
//
|
||||
// no parameters the whole picker, standing where the anchor in the address
|
||||
// bar puts it. Asked for by the picker itself: on load,
|
||||
// bar puts it. Asked for by the picker itself: on load, and
|
||||
// after a history restore -- which swaps the history element
|
||||
// straight in and processes no out-of-band elements -- and
|
||||
// when a worker first appears under a page boot.html
|
||||
// composed.
|
||||
// straight in and processes no out-of-band elements.
|
||||
// at=ch3 the verses of that chapter, and land on the chapter.
|
||||
// at=v3_16 the same verses, that one selected, and land on the verse.
|
||||
//
|
||||
@@ -490,9 +483,8 @@ router.get('/api/bookmark', () => bookmarkFragment(''));
|
||||
// is rendered at 1:1 like any other. The page then asks this route for itself,
|
||||
// and that request is the one carrying the anchor.
|
||||
//
|
||||
// Which also puts the picker where it belongs after a back or forward, and
|
||||
// under a page boot.html composed once a worker turns up, for nothing extra:
|
||||
// those requests carry the address bar too.
|
||||
// Which also puts the picker where it belongs after a back or forward, for
|
||||
// nothing extra: those requests carry the address bar too.
|
||||
//
|
||||
// Two response headers carry what a change means beyond the options:
|
||||
//
|
||||
@@ -734,13 +726,11 @@ function getBookmark() {
|
||||
.then((mark) => mark || DEFAULT_BOOKMARK);
|
||||
}
|
||||
|
||||
// The navbar slot, and the toast that says what just happened to it. The
|
||||
// toast rides out of band, so one response updates the slot and raises it
|
||||
// without the page having to ask for the second one -- same template and same
|
||||
// table api/toast answers from, so the two cannot drift apart.
|
||||
async function bookmarkFragment(say) {
|
||||
let html = await view.render('bookmark.njk', { mark: await getBookmark() });
|
||||
if (say) {
|
||||
// Out of band, so one response updates the navbar slot and raises the
|
||||
// toast without the page having to ask for the second one. Same template
|
||||
// and same table api/toast answers from, so the two cannot drift apart.
|
||||
html += '\n' + await toastFragment(say, true);
|
||||
}
|
||||
return html;
|
||||
return await view.render('bookmark.njk', { mark: await getBookmark() })
|
||||
+ '\n' + await toastFragment(say, true);
|
||||
}
|
||||
@@ -2,22 +2,12 @@
|
||||
worker holds, and returned on its own by api/bookmark after a write. Both
|
||||
paths must produce the same element for the swap to be seamless.
|
||||
|
||||
mark is null in one case only: boot.html composed this page, so there was
|
||||
no worker to ask. The slot is then hidden rather than absent and fetches
|
||||
itself once app.js reports a worker is controlling -- which is why the htmx
|
||||
attributes are on the empty state and not the filled one. A filled slot is
|
||||
only ever replaced by the modal's form, which names #bookmark itself.
|
||||
|
||||
Empty state is d-none, not the hidden attribute: Bootstrap declares .d-flex
|
||||
after Reboot's [hidden] rule, both !important and at the same specificity,
|
||||
so the utility would win. #}
|
||||
<span class="bookmark navbar-text {% if mark %}d-flex{% else %}d-none{% endif %} align-items-center gap-1"
|
||||
id="bookmark"
|
||||
{%- if not mark %}
|
||||
hx-get="api/bookmark" hx-trigger="worker-ready from:body"
|
||||
hx-target="this" hx-swap="outerHTML"
|
||||
{%- endif %}>
|
||||
{%- if mark %}
|
||||
Always filled: getBookmark() answers with the default when nothing has been
|
||||
set and when storage cannot be read at all, so there is no empty state to
|
||||
render. There was one -- a hidden slot that fetched itself once a worker
|
||||
turned up -- for pages boot.html composed before there was a worker to ask.
|
||||
Nothing composes a page but the worker now, and the worker always knows. #}
|
||||
<span class="bookmark navbar-text d-flex align-items-center gap-1" id="bookmark">
|
||||
{# A word and a colon, not a badge: this is one line of text that happens to
|
||||
end in a link, and a pill in front of it read as a control. Outside the
|
||||
anchor so that only the reference is underlined -- Reboot underlines an
|
||||
@@ -28,5 +18,4 @@
|
||||
anchor, which a swap into #content would not do. #}
|
||||
<a class="bookmark-link link-light text-truncate" href="{{ mark.href }}"
|
||||
hx-boost="false">{{ mark.ref }}</a>
|
||||
{%- endif %}
|
||||
</span>
|
||||
@@ -28,8 +28,7 @@
|
||||
line, so one response moves both.
|
||||
a restore htmx swaps the history element straight in and processes
|
||||
no out-of-band elements at all, so the picker asks for
|
||||
itself instead. worker-ready is in the same trigger, for a
|
||||
page boot.html composed before there was a worker to ask.
|
||||
itself instead.
|
||||
an anchor a link to GEN#v3_16 should open with the picker reading
|
||||
3:16, and the fragment never reaches the worker: a browser
|
||||
asks for /GEN and keeps the rest. So a picker that arrived
|
||||
@@ -57,7 +56,7 @@
|
||||
id="reference"
|
||||
{%- if oob %} hx-swap-oob="true"{% endif %}
|
||||
hx-get="api/reference"
|
||||
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}worker-ready from:body, htmx:historyRestore from:body"
|
||||
hx-trigger="{% if reference and not settled %}load[!!location.hash], {% endif %}htmx:historyRestore from:body"
|
||||
hx-target="this" hx-swap="outerHTML">
|
||||
{%- if reference %}
|
||||
<span class="reference-book text-light fw-semibold"
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
without this document being rebuilt, so the value only becomes current on
|
||||
the next full load -- which is the reload you would do to check it anyway.
|
||||
|
||||
version is null in one case: boot.html composed this page, so there was no
|
||||
worker to ask. Nothing is rendered at all then, which is the honest answer
|
||||
-- no worker means no version, and the bookmark slot in the navbar is empty
|
||||
for the same reason. app.js finds no element and leaves it at that.
|
||||
|
||||
Rendered on every page and shown under the book list alone. Populating it
|
||||
only for the index would look like the smaller change and would not work:
|
||||
the shell is composed once per full navigation, and a boosted link to the
|
||||
@@ -22,10 +17,8 @@
|
||||
decides, not the composer -- app.js looks for the index in #content after
|
||||
every swap. The body class this was rendered with cannot be asked, for the
|
||||
same reason: it goes stale the moment a link is clicked. #}
|
||||
{%- if version %}
|
||||
<div class="app-version d-none text-center mt-5 mb-2" id="app-version">
|
||||
<p class="app-version-digests form-text m-0">
|
||||
shell {{ version.shell }} · library {{ version.library }}
|
||||
</p>
|
||||
</div>
|
||||
{%- endif %}
|
||||
Reference in new issue
Block a user