Permalink
Browse files

add a Service Worker to serve /search from cache

For more details about Service Workers, see
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

The new /placeholder.html handler serves the placeholder template with
inlined CSS and the %q%/%q_escaped% placeholders which will then be
replaced by the service worker javascript code with the current query.

instant.js registers the service worker, which will download assets to
cache and handle any subsequent requests to /search, getting rid of an
entire round trip.

fixes #69
  • Loading branch information...
stapelberg committed Jul 7, 2016
1 parent 9dbf468 commit 7f31aef402cb782056e290a797f224171f4af270
Showing with 124 additions and 2 deletions.
  1. +12 −0 cmd/dcs-web/dcs-web.go
  2. +1 −1 cmd/dcs-web/templates/footer.html
  3. +4 −0 nginx.example
  4. +1 −1 static/Makefile
  5. +4 −0 static/instant.js
  6. +102 −0 static/service-worker.js
View
@@ -391,6 +391,18 @@ func main() {
http.HandleFunc("/queryz", QueryzHandler)
http.HandleFunc("/track", Track)
http.HandleFunc("/events/", EventsHandler)
+ // Used by the service worker.
+ http.HandleFunc("/placeholder.html", func(w http.ResponseWriter, r *http.Request) {
+ if err := common.Templates.ExecuteTemplate(w, "placeholder.html", map[string]interface{}{
+ "criticalcss": common.CriticalCss,
+ "version": common.Version,
+ "q": "%q%",
+ "q_escaped": "%q_escaped%",
+ }); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ })
http.Handle("/instantws", websocket.Handler(InstantServer))
http.Handle("/metrics", prometheus.Handler())
@@ -17,6 +17,6 @@
<script type="text/javascript" src="/loadCSS.min.js"></script>
<script type="text/javascript" src="/cssrelpreload.min.js"></script>
<script type="text/javascript" src="/jquery.min.js"></script>
-<script type="text/javascript" src="/instant.min.js?5"></script>
+<script type="text/javascript" src="/instant.min.js?6"></script>
</body>
</html>
View
@@ -190,6 +190,10 @@ server {
proxy_pass http://dcsweb;
}
+ location /placeholder.html {
+ proxy_pass http://dcsweb;
+ }
+
# Everything else must be a static page, so we directly deliver (with
# appropriate caching headers).
location /research/ {
View
@@ -1,4 +1,4 @@
-all: instant.min.js instant.min.js.gz debcodesearch.min.css.gz non-critical.min.css non-critical.min.css.gz critical.min.css critical.min.css.gz loadCSS.min.js loadCSS.min.js.gz cssrelpreload.min.js cssrelpreload.min.js.gz url-search-params.min.js.gz
+all: instant.min.js instant.min.js.gz debcodesearch.min.css.gz non-critical.min.css non-critical.min.css.gz critical.min.css critical.min.css.gz loadCSS.min.js loadCSS.min.js.gz cssrelpreload.min.js cssrelpreload.min.js.gz url-search-params.min.js.gz service-worker.min.js service-worker.min.js.gz
%.gz: %
echo "ZÖPFLI $<"
View
@@ -533,6 +533,10 @@ function changeGrouping() {
}
$(window).load(function() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/service-worker.min.js');
+ }
+
// Pressing “/” anywhere on the page focuses the search field.
$(document).keydown(function(e) {
if (e.which == 191) {
View
@@ -0,0 +1,102 @@
+// vim:ts=4:sw=4:et
+var version = 'v1/';
+
+var assets = {
+ '/non-critical.min.css': true,
+ '/Pics/openlogo-50.svg': true,
+ '/Pics/rackspace.svg': true,
+ '/jquery.min.js': true,
+ '/url-search-params.min.js': true,
+ '/loadCSS.min.js': true,
+ '/cssrelpreload.min.js': true,
+ '/instant.min.js?6': true,
+ // Only cache fonts in woff2 format, all browsers which support service
+ // workers also support woff2.
+ '/Inconsolata.woff2': true,
+ '/Roboto-Regular.woff2': true,
+ '/Roboto-Bold.woff2': true,
+ '/placeholder.html': true
+};
+
+self.addEventListener("install", function(event) {
+ event.waitUntil(
+ caches
+ .open(version + 'assets')
+ .then(function(cache) {
+ return cache.addAll(Object.keys(assets));
+ })
+ );
+});
+
+self.addEventListener("activate", function(event) {
+ // The new version of the service worker is activated, superseding any old
+ // version. Go through the cache and delete all assets whose key doesn’t
+ // start with the current version prefix.
+ event.waitUntil(
+ caches
+ .keys()
+ .then(function(keys) {
+ return Promise.all(
+ keys
+ .filter(function(key) {
+ return !key.startsWith(version);
+ })
+ .map(function(key) {
+ return caches['delete'](key);
+ })
+ );
+ })
+ );
+});
+
+self.addEventListener("fetch", function(event) {
+ if (event.request.method !== 'GET') {
+ return;
+ }
+ var u = new URL(event.request.url);
+ if (assets[u.pathname + u.search] === true) {
+ event.respondWith(caches.match(event.request).then(function(response) {
+ // Defense in depth: in case the cache request fails, fall back to
+ // fetching the request.
+ return response || fetch(event.request);
+ }));
+ return;
+ }
+ if (u.pathname === '/search') {
+ event.respondWith(caches.match('/placeholder.html').then(function(response) {
+ if (!response) {
+ return fetch(event.request);
+ }
+ // URLSearchParams is not available on all browsers yet.
+ var searchParams = u.search.slice(1).split('&');
+ var q;
+ var qEscaped;
+ for (var i = 0, len = searchParams.length; i < len; i++) {
+ if (searchParams[i].indexOf('q=') === 0) {
+ qEscaped = searchParams[i].substr('q='.length);
+ q = decodeURIComponent(qEscaped.replace(/\+/g, ' '));
+ break;
+ }
+ }
+ if (q === undefined) {
+ return response;
+ }
+
+ var init = {
+ status: response.status,
+ statusText: response.statusText,
+ headers: {},
+ };
+ response.headers.forEach(function(v, k) {
+ init.headers[k] = v;
+ });
+ return response.text().then(function(body) {
+ var replaced = body.replace(/%q%/g, q);
+ replaced = replaced.replace(/%q_escaped%/g, qEscaped);
+ return new Response(replaced, init);
+ });
+ }));
+ return;
+ }
+ return;
+});

0 comments on commit 7f31aef

Please sign in to comment.