Skip to content

Commit e72cbe3

Browse files
feature (fastboot): optimise boot sequence
1 parent 6e0037b commit e72cbe3

File tree

5 files changed

+216
-313
lines changed

5 files changed

+216
-313
lines changed

Jenkinsfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pipeline {
2828
sh "make build_frontend"
2929
}
3030
docker.image("golang:1.24-bookworm").inside("--user=root") {
31+
sh "apt update -y && apt install -y libbrotli-dev brotli"
3132
sh "sed -i 's|plg_image_c|plg_image_golang|' server/plugin/index.go"
3233
sh "make build_init"
3334
sh "make build_backend"

public/assets/sw.js

Lines changed: 11 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,5 @@
11
let VERSION = null;
22

3-
/*
4-
* This Service Worker is an optional optimisation to load the app faster.
5-
* Whenever using raw es module without any build, we had a large number
6-
* of assets getting through the network. When we looked through the
7-
* developer console -> network, and look at the timing, 98% of the time
8-
* was spent "waiting for the server response".
9-
* HTTP2/3 should solve that issue but we don't control the proxy side of
10-
* things of how people install Filestash, hence the idea to bulk download
11-
* as much as we can through SSE, store it onto a cache and get our
12-
* service worker to inject the response.
13-
* This approach alone make the app a lot faster to load but relies on
14-
* the server being able to bundle our assets via SSE.
15-
*
16-
* TODO:
17-
* - wait until browser support DecompressionStream("brotli") natively
18-
* and use that. As of 2025, downloading a brotli decompress library
19-
* make the gain br / gz negative for our app
20-
* - wait until Firefox support SSE within service worker. As of 2025,
21-
* someone was implementing it in Firefox but it's not everywhere yet
22-
* Once that's done, we want to be 100% sure everything is working great
23-
*/
24-
253
self.addEventListener("install", (event) => {
264
if (!self.EventSource) throw new Error("turboload not supported on this platform");
275

@@ -58,104 +36,27 @@ self.addEventListener("message", (event) => {
5836
);
5937
});
6038

61-
async function handlePreloadMessage(chunks, clear, version, resolve, reject) {
39+
async function handlePreloadMessage(imports, clear, version, resolve, reject) {
6240
VERSION = version;
63-
const cleanup = [];
6441
try {
65-
let execHTTP = true;
6642
await caches.keys().then(async(names) => {
67-
for (let i=0; i<names.length; i++) {
68-
if (names[i] === VERSION && !clear) {
69-
execHTTP = false;
70-
return;
71-
}
43+
for (let i = 0; i<names.length; i++) {
7244
await caches.delete(names[i]);
7345
}
7446
});
75-
if (execHTTP) {
76-
const cache = await caches.open(VERSION);
77-
chunks = await Promise.all(chunks.map(async(urls) => {
78-
const missing = [];
79-
await Promise.all(urls.map(async(url) => {
80-
if (!await cache.match(location.origin + url)) missing.push(url);
81-
}));
82-
return missing;
83-
}));
84-
if (chunks.filter((urls) => urls.length > 0).length > 0) {
85-
await Promise.all(chunks.map((urls) => {
86-
return preload({ urls, cache, cleanup });
87-
}));
88-
}
47+
const cache = await caches.open(VERSION);
48+
for (const path in imports) {
49+
let mime = "application/octet-stream";
50+
if (path.endsWith(".css")) mime = "text/css";
51+
else if (path.endsWith(".js")) mime = "application/javascript";
52+
await cache.put(location.origin + path, new Response(
53+
new Blob([imports[path]]),
54+
{ headers: { "Content-Type": mime } },
55+
));
8956
}
9057
resolve();
9158
} catch (err) {
9259
console.log("ERR", err);
9360
reject(err);
94-
} finally {
95-
cleanup.forEach((fn) => fn());
9661
}
9762
};
98-
99-
async function preload({ urls, cache, cleanup }) {
100-
const evtsrc = new self.EventSource("/assets/bundle?" + urls.map((url) => `url=${url}`).join("&"));
101-
cleanup.push(() => evtsrc.close());
102-
103-
let i = 0;
104-
const messageHandler = async(resolve, event, decoder) => {
105-
const url = event.lastEventId;
106-
let mime = "application/octet-stream";
107-
if (url.endsWith(".css")) mime = "text/css";
108-
else if (url.endsWith(".js")) mime = "application/javascript";
109-
110-
i += 1;
111-
await cache.put(
112-
location.origin + url,
113-
new Response(
114-
decoder(new Blob([base128Decode(event.data)]).stream()),
115-
{ headers: { "Content-Type": mime } },
116-
),
117-
);
118-
if (i === urls.length) {
119-
resolve();
120-
}
121-
};
122-
const errorHandler = (reject, err) => {
123-
reject(err);
124-
};
125-
126-
await new Promise((resolve, reject) => {
127-
evtsrc.addEventListener("static::raw", (event) => messageHandler(
128-
resolve,
129-
event,
130-
(stream) => stream,
131-
));
132-
evtsrc.addEventListener("static::gzip", (event) => messageHandler(
133-
resolve,
134-
event,
135-
(stream) => stream.pipeThrough(new DecompressionStream("gzip")),
136-
));
137-
evtsrc.onerror = (err) => {
138-
if (i === urls.length) return;
139-
errorHandler(reject, err);
140-
};
141-
});
142-
}
143-
144-
function base128Decode(s) { // encoder is in server/ctrl/static.go -> encodeB128
145-
const out = new Uint8Array(Math.floor((s.length * 7) / 8) + 1);
146-
let acc = 0;
147-
let bits = 0;
148-
let oi = 0;
149-
for (let i = 0; i < s.length; i++) {
150-
const ch = s.charCodeAt(i);
151-
const digit = ch & 0x7F; // undo 0x80 masking for NUL/LF/CR
152-
acc = (acc << 7) | digit;
153-
bits += 7;
154-
while (bits >= 8) {
155-
bits -= 8;
156-
out[oi++] = (acc >> bits) & 0xFF;
157-
acc &= (1 << bits) - 1;
158-
}
159-
}
160-
return out.subarray(0, oi);
161-
}

public/index.frontoffice.html

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
<link rel="stylesheet" href="custom.css">
2020
<link rel="stylesheet" href="./assets/{{ .version }}/css/designsystem.css">
2121
</template>
22-
2322
<template id="body">
2423
<script type="module" src="./assets/{{ .version }}/components/loader.js"></script>
2524
<script type="module">
@@ -30,36 +29,47 @@
3029
beforeStart: import("{{ .base }}assets/{{ .version }}/boot/ctrl_boot_frontoffice.js"),
3130
});
3231
</script>
33-
3432
<component-modal></component-modal>
3533
<script type="module" src="./assets/{{ .version }}/components/modal.js" defer></script>
36-
3734
<component-notification></component-notification>
3835
<script type="module" src="./assets/{{ .version }}/components/notification.js" defer></script>
3936
</template>
4037

41-
<script id="preload" type="application/json">{{ .preload }}</script>
42-
4338
<script type="module">
44-
function boot() {
39+
function liftoff() {
4540
document.head.appendChild(document.querySelector("template#head").content);
4641
document.body.appendChild(document.querySelector("template#body").content);
4742
}
48-
49-
if ("serviceWorker" in navigator) {
50-
const URLS = JSON.parse(document.getElementById("preload").textContent);
43+
async function ignitionSequence() {
44+
if (!("serviceWorker" in navigator)) return
5145
try {
52-
const register = await navigator.serviceWorker.register("sw.js");
53-
await new Promise((resolve) => {
54-
register.active ?
55-
resolve() :
46+
window.bundler = (function () {
47+
let modules = [];
48+
return {
49+
register: (path, code) => modules[path] = code,
50+
state: () => modules,
51+
};
52+
})();
53+
const [register] = await Promise.all([
54+
navigator.serviceWorker.register("sw.js").then((register) => new Promise((resolve) => {
55+
register.active ?
56+
resolve(register) :
5657
navigator.serviceWorker.addEventListener("controllerchange", () => {
57-
resolve();
58+
resolve(register);
5859
});
59-
});
60+
})),
61+
new Promise((resolve, reject) => {
62+
const $script = document.createElement("script");
63+
$script.type = "module";
64+
$script.src = "./assets/bundle.js?version={{ slice .version 0 7 }}::{{ .hash }}";
65+
document.head.appendChild($script);
66+
$script.onload = resolve;
67+
$script.onerror = reject;
68+
}),
69+
]);
6070
register.active.postMessage({
6171
"type": "preload",
62-
"payload": URLS,
72+
"payload": bundler.state(),
6373
"version": "{{ slice .version 0 7 }}::{{ .hash }}",
6474
"clear": {{ .clear }},
6575
});
@@ -71,14 +81,40 @@
7181
}));
7282
} catch (err) { console.error(err); }
7383
}
74-
boot();
84+
85+
//
86+
//
87+
//
88+
//
89+
// /\
90+
// / \
91+
// || / \
92+
// || /______\
93+
// ||| |
94+
// | | |
95+
// | | |
96+
// |__|________|
97+
// |___________|
98+
// | | |
99+
// |__| || |\
100+
// ||| || | \
101+
// /||| || | \
102+
// /_|||...||...|___\
103+
// |||::::::::|
104+
// || \::::::/
105+
// || ||__||
106+
// || ||
107+
// || \\_______________
108+
// _______________||______`---------------
109+
// |
110+
// | |
111+
await ignitionSequence() // |
112+
// |
113+
liftoff() // |
114+
// | |
115+
// |_____________________________________________|
75116
</script>
76117

77-
<noscript>
78-
<div style="text-align:center;font-family:monospace;margin-top:5%;font-size:15px;">
79-
<h2>Error: Javascript is off</h2>
80-
<p>You need to enable Javascript to run this application</p>
81-
</div>
82-
</noscript>
118+
<noscript><div style="text-align:center;font-family:monospace;margin-top:5%;font-size:15px;"><h2>Error: Javascript is off</h2></div></noscript>
83119
</body>
84120
</html>

0 commit comments

Comments
 (0)