 So, let's get started. So, what you see here is the thing that we have built a while ago. It is a website with no really important content, but you get nice animations when you go to a different site, and as you can see, we are only requesting a partial, which is just a single line of HTML that is being swapped in, and all the rest is JavaScript and transition magic. And this is what we want to try to make work offline. Oh, somebody went full screen. That is not good. Let's not go full screen. So, the first thing that I'm going to do is I'm going to register a service worker, and because I'm a fast typer, I'm done. You cheat. You big old cheat. So, what I usually start with is a string for a version. Wait, wait, wait, wait, wait. Go back, go back, go back. So, you've got a, right, if service worker navigator, let's talk about that just a That is progressive enhancement. So, basically, it checks if the navigator object has the service worker call, and if not, we're not going to do anything. If it does, we can actually register a service worker. So, we don't register the service worker. We tell the scope, you're going to control everything from forward slash. You're going to control every request. Everything. Everything. Yes. Right. As you were. So, version string, I usually do because it is a nice place to force a reload of the service worker. So, the service worker file is going to be checked if it has changed, if it is byte equivalent. Correct. It is not going to be reloaded. If it's something changed, a whole new service worker is going to be spun up. So, a version string is a very nice place to force this kind of behavior. Now, good news. If you set far future caching on your JavaScript files, at most, the spec says you're going to wait 24 hours before Chrome, or in fact, any browser that supports service workers will go and try and get a new version of your service worker. But you could be stuck with a service worker version for 24 hours. So, typically on a service worker, you want to make sure that you're serving it. For example, today, we changed the schedule of Chrome Dev Summit. If we had been stuck for 24 hours on the same service worker, that would be awkward, right? So, there we go. So, the first thing we're going to do on install basically always looks the same for me. I usually do a skip waiting in here, which means. Sell.skipwaiting. Type O. He caught me. So, we're not going to wait for service worker shut down. We're going to take over immediately, which is also what we have to do in activate. Okay. So, let's talk very quickly about the life cycle. So, when a service worker is downloaded, it goes through an installation step. I think Shae wrote a really good article about that. Yeah. I mean, Dev, oh, yeah. So, there's a brand new article on developers.google.com slash web slash fundamentals, I think it probably is. It's probably on fundamentals. Yeah, it's probably on fundamentals. And I'll find it and I'll post it into the Slack in a moment. But basically, it talks through the life cycle. But in the short version, you have an installation step. When a service worker is finished installing, it will activate as soon as any, as soon as it's able to, which largely involves any connected clients they're called, have disconnected, and it becomes available. Otherwise, you have to wait until they've all disconnected. You've got an on install. I'm not going to skip waiting. On activate, I'm going to claim all my clients. So, my new service worker is active now, but it hasn't taken care of all the open tabs, if you have multiple ones. Self clients claim says, give me all the tabs that they are, and I'm going to take care of them from now on. Okay. So, if you've got an existing service worker, you're just basically kicking the old one out immediately and saying, I'm the new one. I will handle everything from this point on. Okay. And for the on fetch, for now, I'm just going to say, I'm just going to do a, a knob service worker just as a path through. So, I'm going to respond with the fetch. So, I'm basically not going to do any offline logic, just any request the page does. I'm going to do the actual network and pass it back. So, if we reload this side now, we see the service worker just got installed, which is good. And now on reload, we should see every request twice, because the website does a request, for example, the scview file, which gets to the service worker, and the service worker does the actual fetch, which is why it also shows up down here. So, everything is still working the same, but we have a service worker now in between our website. So, it's really worth pointing out this. That little cog there is the, is the thing you're looking for. And also, if you have a from service worker, that is good things to watch out for. But you are seeing that two steps. Something went to the service worker, the service worker did the fetch, and then it comes back. So, a pretty common way is to download the files that you know the user is going to need on install. And that's exactly what we're going to do. So, we're going to define an assets array, and these are the files that we need. Dude. Boom. You're like, today you're like the king of snippets, aren't you? And I was like, how long has this taken you? And he's like, yeah, I've practiced and I can do it in like 11 minutes. I didn't realize it was all snippets. These are actually the two snippets I have. You're done with snippets now, because it's boring tidying this out, I thought. There's no semicolon at the end of that assets declaration. Well, it's not technically wrong. It's just. Awkward. Oh. If the linting was switched on, it would have a real issue right now. But now, now, now, now, we're going to be interesting. We're going to write async function, oh yeah. Which now everybody knows will run synchronously. Exactly. From the quiz. Until I do await and do cache's open static. Now, you've got to explain what's going on here. Exactly. So async function, just think about it as a function. It is actually a function that returns a promise. Just that we're on the same page. OK. But by putting your sort of marking it by saying it's an async function. Because only inside async functions we can use await. And we want to do that because it's cool. And that means that this function actually returns a promise. So everything in here will basically be kind of rewritten into a promise chain. But it's so much more readable now. OK. So we're saying we've got an async function. We're saying open the caches. So usually you do like a .then afterwards. And you get the cache. And then you can do something inside the .then. But now we can actually just await for it. And this is the same as a .then, but puts it into a variable, which is just. So it reads like synchronous code. Like I want the cache, and then I want to do something else and something else and something else. But under the hood, it's magically doing sort of promisey type stuff. So the next thing is another await where we can do cache add all assets. So this step is going to wait until we have downloaded all the assets in our array, put it into the cache, and then we will return skip waiting. So the ultimate value of this promise that the async function returns is going to be self skip waiting, which is what we want to pass into wait until because we're done with our setup. But before that, all the other steps are going to happen. And we can check this works by reloading the page. Because now the service worker, oh, remind me, I'm going to write all of async functions. If I forget these two parentheses at the end, shout, because it's a painful debug. But we want to invoke this function right away. Ew. So let's do this again. Sorry, that was involuntary. So service worker, and we see now it downloaded all the files again. Which reminds me, when we do these, it's not production code. I'm not saying you're writing by code. Awkward. But there is a sense in which we do this stuff to kind of show the approach that you take. This isn't necessarily stuff we would say, just take this, copy paste, and you're good to go. That is important. It's worth mentioning. So now that we have things in our cache, we know that everything from our static folder is going to be in our cache 100% because we are downloading it on install. And this is what I want to take advantage of in our own fetch. Well, can you go back and reload the page and then show them the cache inspection in DevTools? Really useful. So at this point, we should have all these assets in the cache, right? So we have the cache store. On the application tab, we have the cache storage. And we can go and have our static cache, and all the files are in here. So this is really useful to see if everything actually landed. Can you show the response? No, you can't. I still don't know why. I don't know what this thing is good for. I should say OK, but it just doesn't like you right now. I am in Canary because async functions are only in Canary, so there might very well be bugs right now. But you normally see the fact that it was an OK request kind of came through. So we are now in the on fetch. And what I want to do is if this request is somewhere for something in static, I don't even want to bother with going to the actual network because I know I have it ready. So something that I kept finding myself doing is actually parsing the URL. So event.request.url is the request URL. But it's just a string. I actually want to have it separate into the query part, the path, the domain, and all these kind of things. So I'm just going to attach a new thing to the event, which is parse.url, which is a nice URL object. Because what I can do now is event.parse.url path name starts. They don't add that to the spec. You're going to feel really awkward. So basically, what I'm checking is if the pass name starts with static, I'll give it some special handling. Namely, I'm going to respond with a cache.match event request. So I'm looking at the cache for the request object and going to respond with it. And I know it's going to be there because we are installing that. So we install these assets. And then when we get a fetch from the page that says, I want whatever URL, hopefully we're going to find it. If the URL starts with slash static, we go find it from the cache. We're pretty confident. Otherwise, we're going to stay with fetching for now. So what should happen in our network tab is that all these server-circuit requests should disappear. So I'm going to reload once, which is going to trigger a reinstall of the server-circuit. So this is all the same. The new server-circuit gets installed. And if I reload again, all the server-circuit requests are gone because we're loading from cache now. Now, here's an interesting one as well. That bottom request there is the service worker trying to do an update for a new version, trying to find, see if there's been anything. Checking if it's byte equivalent or not. Yeah. So you will see that kind of going, the service worker looks for the new service worker. So that's normal. So for the rest, right now, if there's a request for something that we don't have in the cache, for example, our index HTML, it goes to the network and just gives it back to the browser and then it just disappears. We don't save it anywhere. Right. So this is the idea. OK, so you've got those. How many is it? Five. Five assets. If I request something that's not in there, we go and fetch it, but then we just kind of it's, if we're offline, we wouldn't have it for next time. Exactly. And that's what we're going to change. I like to write functions for the different caching strategies. So in this case, I'm going to do a stale while revalidate. Rallinade. There we go. So what this means is, stale while revalidate means, if we have something in the cache, return it right away, even if it is stale, and in the background, fetch it from the server and update the cache. So we see this typically on social networking sites where they kind of go, here's all the posts that I've got stored. Enjoy those. Meanwhile, I'm also going to go off and get the latest set of posts. And when I've got those, bam, I'll update my collection for you. So the nice thing about the strategy is that we're definitely going to do both. We're going to do fetch, and we're going to do check the cache, because we're going to need both in either case. So we're going to. Somebody's asking on the chat, and if you're not in the chat and want to join in, it is the chromiumdev.slack.com. We are in the general channel. Somebody's asking if the code is going to go on to GitHub? Totally. The answer is yes. I will post the link in the chat if somebody else doesn't get there before me. We have a UI element samples repo, which has all the elements that we've built, all the stuff we've built. Supercharged, so far. All the supercharged stuff so far for you to look through. And this will be no different, so. It will be in there. I might clean it up a little bit afterwards, some comments. Just to remind you what I was actually doing. Yeah. Because I'm not going to write comments now, like who does? So we have two things that we can do now. First, we have to figure out what to respond with. And this is something that I want to give to you. In respond with, try to get to the response as fast as possible and nothing else. Caching, you can worry about in the wait until. Respond with should be fast. So now you're going to have to explain the difference between respond with and wait until. So we have event respond with and event wait until. So respond with expects a promise that results to the thing that is going to be given to the browser as the response to the request. So for example, you could respond with the fetch. Yeah. And a fetch is going to return a promise which will ultimately result in the content of the thing that was being fetched. Which is a response, a response object. Wait until, however, keeps the service worker alive if you want to do some further work, like opening a cache and putting stuff into that cache. Otherwise, the service worker might get killed before you actually save it. Yes. Now this is an interesting part of service worker work is that there's no promise given or I shouldn't say promise. There's no guarantees given that a service worker won't be spun down. So say, for example, you've loaded the page. Even if the tab is open, it can be spun down. So once Sermon had done his reload of the page, the browser is well within its rights to say, I no longer need this service worker. Get rid of it. Which, by the way, is by DevTools says now that service worker termination by time out, time out was canceled because DevTools is attached. So it was going to shut down the service worker. But because DevTools is attached, it said I'm not going to do that in case you want to inspect what's happened, in case you just want to have a look at the service worker. Or because when you log an object from the service worker, you can't expand it anymore if the service worker has been killed because the context is gone. So it's just a service from the Chrome people to us that we can do debugging. And so they wait until is your programmatic way of saying, don't shut down the service worker yet. I have some stuff to do, which in this case, I assume is going to be putting things into a cache. Well, in response with, no. And respond, but we're just going to worry about what the response is. So our response in this case is going to be a promise.race. Because whatever we have, the fetched version or the cached version, I assume we're trying whatever is there faster. So promise.race is a function that takes an array of promises and gives back whatever promises settles first. OK. So you've got a fetch, you've got to look in the cache, you're going to say, don't care, pick one, whichever is there first. Most of the time, I would expect that to be the cached version. Yes. However, if we are offline, fetch rejects. And promise.race doesn't care if it's a reject or a fulfill, it will turn whatever settles first, which includes both. So when we're offline, fetch is almost immediately going to throw and be like, I'm offline, I can't do anything. And our promise.race is going to say, OK, here is your rejected promise have fun. Which is not really what we want. So what I do in this case is I'm just going to put a little sneaky catch to the fetched version and say, if you're going to reject, just turn into the cached version too. Nice, that is well-cheeky. Sorry, I'm totally stealing that. So what's going to happen now is, and now we have to think about this. So if the fetched rejects, we're going to get the result of the cached version, which can be a response, but it can also be undefined, which is what caches.match returns. If it doesn't have anything in the cache. So in this case, what I'm going to do is, if the response is not valid, so it is undefined, I'm going to return the fetched version. So this covers all the possible outcomes. Because we also have to think about, there is a way, if we are actually online, the cache could resolve first, but could be undefined because we don't have anything in the cache. And then we want to wait for the network. First glance, what appeared to be quite neat and tidy. You're kind of going, right, fetched and cached. But if the fetched version decides it's offline, and we'll switch to the cached version. But if the cached version doesn't work out for whatever reason, we'll go to the fetched version. Yeah. And now it gets even better. Everybody following along. There's going to be a pop quiz. Oh, we're actually doing a pop quiz. Well, we might actually have a quiz question. Damn. Nobody's going to get the right answer. So the thing is, as I said before, fetch can reject because when we're offline, it's going to reject. So there might be a way that cache responds first with, I got nothing. See Matt Gaunt, who's one of our colleagues, he just says, I was just sick in my mouth. Oh, Gauntie. I'm going to get back to him now. So what I'm going to do now is fetch could reject. So if that happens, if we're down here and fetch reject, we know nothing's in the cache and we're offline. So what are we going to do? Panic. Exactly. That's my code. So what I'm going to do is I'm going to return a new response. Lol, offline. With nothing that we're going to say 4-4, done. Oh, that's such a grown-up way of doing it. I know. I just be like, I am German. OK. I would have been like, yeah, I don't. So let's comment this out for a second. Reload the page. Get the new server, sir. Any errors? Looking good? Looking good? Oh, 4-4 on the local host. What did I do wrong now? Now, at this point, I would try and be his rubber duck. Anybody heard of the rubber duck theory? Yeah, a few hands going up. If you've not heard of it, the idea is that you have a rubber duck on your desk. And when you hit a bug, you introduce yourself to the rubber duck. Hi, I'm Paul. Here's my problem. And as you explain your problem to the rubber duck, in this case, me, you tend to go, oh, oh. I did the thing with the thing. It's fine. Thanks, rubber duck. And you go back and you fix your bug. Often by just talking it through with somebody, apparently even an inanimate duck is enough to help. How's it going? I'm 31. 31? Cashed with a D. It's a bug. It's a typo bug. Thank you. See, I think if you'd had linting on, you'd have got that. Me, me, me, me, me, me, me, me. Yay! A plus. So everything should totally still be working. We're still fetching everything. We're not putting it in the cache yet, so let's fix that. OK, so we have this thing done, which basically does the right thing. Just believe me. I do. I'm just OK. And now we're going to talk about the caching. This one is actually simple. So what we're going to do is we're going to wait for. Is it surmysimple? No, it's actually. Because I'm looking at the other one. So we're going to wait for the fetched version. We're going to wait for our fetch. We're going to open our cache, caches.open. I'm going to call this one dynamic because we're in. So we have the static stuff. This is the stuff that we're kind of going. Oh, we didn't have this to begin with. So maybe, for example, this would be where you'd store your post, the stuff that changes over time, right? Maybe use a generated content. Incidentally, if you're taking this approach, make sure that you have some kind of purging strategy. Otherwise, your caches can grow forevermore. Hashtag topical. There you go. So you want to make sure that you have a way of getting rid of old stale data. So we wait for the response. We wait for our cache. And then we put the response in the cache. Done, almost. As I said before, the fetch can reject if you're offline. So we are just going to catch that error and just not do anything about it. And also, wait, did I miss anything? Hey, you weren't grown up anymore. Oh, I'm so proud of you. OK, I prefer it when it's low. So this should be working, right? So OK, so. Oh, no, we have actually something subtle, which is why I almost forgot about it. So let's imagine this. The cache got nothing. That means that our response with returns our fetched version. And then we get into a wait until and we want to store our fetched version. So we're using fetched version twice. I know this one. I know this one. That doesn't work because the body of a fetch is a stream and you can only consume it once. So what we're going to have to do is we're going to have to do a fetched copy. So this is the same as the fetched version, but the response should be cloned. And then down here, we can wait for the copy instead of the fetched version. So let's reload and see if this works. Reload, new server tracker, and another reload. So we can still go to about. We can still go to contact, all good. And now let's go to the applications panel and go offline. Reload still works. About, cool, contact, cool. Misk is not going to work because we haven't visited Misk just yet. This is just going to put up our great spinner. OK, so what we would want to find a way to kind of handle that one nicely and be like, sorry, can't get this offline. Maybe, not today, though. I mean, it's almost like you're saying with time constraint that I keep interrupting you. It's almost like it's revenge for me for all our other checks. What doesn't work, though, is that we cannot refresh here. It just doesn't work. Because you've all got errors as well. Well, yeah, because I just an unknown error. It's great. So in the dynamic, we have the root page, but we only have the partials for about and contact. So that's why we can only refresh while offline on home, but not about in contact, because we don't have the complete page cache just yet. Yeah, so a bit of background for you. When we made this originally, each individual page, we served the whole thing. And then on the updated version, we only pulled in the bits that we actually needed for the XHR, so that when you go from home to about, we only pull in the little bit that was needed. And so what we're saying is that the thing that's caching, it's only caching the changed bit rather than, if you refresh, it goes, well, I haven't got the full about page or the full Misk page. So we need to figure that one out. So what we're going to do is, and this is where it actually gets exciting, we are actually going to do the templating in the server side now. So we're going to pull in our header partial, our footer partial, and do the assembly of these partials in the server side. The same thing we built on the server side in the server side rendering episode, we're going to put that into the server side. So the first thing that we need is our partials. So I'm just going to sneakily put those in our static cache, which is header.partial.html and also footer. Just so you see what these look like, they're just partials, the start of document with a little bit of, this is the wrong document, but it's similar, let me check where it is. There it is. It's just basically the top of the document and the bottom, and we put our own content in between. And there's a little bit of templating magic, which we're also going to take care of. So now we have the partials in our static cache. And now we need to figure out how, when is this request for a web page, for one of our top level pages, instead of any assets or other links we might add later? That seems like something that doesn't have partial on it and would have gone to the dynamic cache. Actually, we solved this when we built the backend and we have this great regex that does this. Hang on, are you sure you don't want to just reuse the code in the kind of isomorphic way? This is almost what we are doing. Almost? Almost, okay. And I'm not going full isomorphic because I think isomorphic has the flaw that it assumes you have note in the backend, which sometimes, or maybe most of the times, is not the case. So we're going to, when you want to do templating in the backend, choose a template language that has multiple language binding. So whatever you use in the backend, you can use it in the server tracker and the backend. All right, so on the fetch, we're going to see if our top level section regex matches our event parse URL path name. And if that's the case, we can call our function that we are about to write which is going to build the site. If not, we're going to use the existing code that we just wrote. So we are going to write build site, which is going to be interesting. So event respond with, what is going to happen in here? Obviously, we're going to use an async function because they're nice. And I'm going to do these because I'm going to forget them. So the first thing that we need is we're going to need to get a hold of our files. So the files that we need are caches.match, header.partial. So again, those from the static cache. Because we know they're there. The footer. And in here, we are going to basically use our stale wire revalidate function, more or less, with the event parse URL to string plus partial. Got you. So you're basically, okay. I'll show you, go ahead. You need a comma after that one. I do. So actually, that's, and because both caches.match and everything else is going to be a promise, we are going to, we're going to wrap this in promise.all. So this is good. But if you paid attention, you know that stale, while revalidate doesn't actually return a promise or return anything. Yeah. So what we're going to do now is basically, we are going to use this function and write a little wrapper on it that actually returns a promise. And this is one of the nice and yet ugly things. No kidding. So stale, while revalidate, it's so hard. Request. So this is the request URL and this is going to return for all we know. A promise. A promise. I shouldn't mock you. I've, I've done, I make so many typos when I'm typing. It's embarrassing. And there I am being unkind to you. And I never mocked you for it. And yet you do. I'm just unkind, aren't I? Yeah. I feel pretty much. Pretty bad about that. So we're going to call our old function which takes an event. We're just going to build our own event now. We're just going to pretend to be an event object even though we are not. Because the only things of event that we're using inside the stale, while revalidate function are respond with, wait until, and request. Wow. So our request is going to be request because interestingly both fetch and cache take both request objects but also strings. So this will just work. Great. Our respond with is going to be our resolve function. So that means whatever this thing, you can see where I'm pointing it on my screen. Yeah, I was going to say. I'm a genius. Well, this thing. So whatever this thing resolves to, it's going to be passed into respond with, which now is the resolve function of our new promise which up here will give back the value. And also we need the actual wait until so we can't really create that so we have to pass it in. So wait until, so we have to put event, wait until dot bind event. Ew. However, it's going to get better because I'm never writing a service worker again. Service workers are hard. So the thing is the service workers are hard. Mine don't look anything like this. Mine are really like, And they don't work. That is true. They. So the thing is something that just got fixed. I'm so sorry. I'm so sorry. I'm going, I'm done. The thing that just got changed in the spec but it's all in the browsers is that right now you can't call wait until inside respond with all the other way around. Okay. Let me do it at the top level in the same tick that the event got dispatched. So we are going to have to do our own wait until. And what I found out is a kind of dirty hack which I'm just going to use until this change lands. I'm going to put a new promise into wait until and just save the result function on a variable and just use that instead. So that means that the service worker will now stay alive until the my wait until will be called and all the browser should be happy even though we are technically calling a wait until function even though it's our own inside respond with. The last thing I want to fix here is that we are appending partial to the string but that isn't technically always correct because this might already be a partial request. So that it would be question mark partial question mark partial which we don't want. The good thing is that we already have a parsed URL, a URL object which has search params of not caps lock search params and we're just going to set partial. But that means if it's already said nothing happens if it's not said it will add the question mark partial to our URL and we will always get the partial thing. Okay, so it's okay. We got our files. We do. The last thing we're going to need is actually get the contents out of our responses. So our contents are the files and of all of those we're going to get the text. Okay, so you're making this request, the header, the footer, the actual bit in the middle and now you're mapping that by saying, okay, we each one of those get the text for each of those. Exactly, however, text is a response so once again we have to put this in a promise.all. Because again, fetch resolves when you get the headers and the metadata but then the body is a stream. So dot text basically returns a promise that reads the entire stream, puts it in a string and gives it back to you. So now we have the contents. Our content, the thing that we're actually going to pass back is the contents joined because it's just three strings and we're going to concatenate them and then we're going to return a new response with our content. So this is basically how we do the templating inside our service worker. So let's reload this. You're going to get the new service worker in or not, but I missed something. Yeah, that shouldn't stop it from working though. Thanks, though. Hmm, offline, you checked the offline box. No, I did uncheck it here. Oh, that's an interesting bug. Usually those are synchronized but apparently they're not. Thank you. I got that, but it's still not working, is it? Okay, I'm just going to unregister my service worker because something is clearly iffy here. Stop. It does happen sometimes when you use Canary, it can happen. Oh, there's like three service workers installed now. We should be on the safe side. Ah, interesting. How's your MIME type working for you? So as you can see, that's exactly what it is. So the default MIME type of response is text plain which is really not what we want. So in our response, we have to add headers, content type, text HTML, reload, new service worker, reload, hooray, but nothing works which is expected because I didn't do the templating. Oh. So I still have my template expressions in here so now we're actually going to load the templating library. Oh, if we use handlebars, right? I don't anymore because handlebars turns out to be like 80 kilobytes and I found a nice little tenant library called DOT which I thought was really nice and not just dot, dot. Well, it is DO capital T. So I don't know how to pronounce it. Dot. Dot too. All right. And this is one of the bits where I say, this is a production code because in this version. So Dan keeps coming up in the chat, by the way. People like really clarify what we mean by it's not production code. So I basically exposed the entire node modules folder. Yeah, why don't you? You don't do that in production. Please don't. But in this case, it's just the easiest way and you don't have to actually think about my build steps in the background, how I'm actually bundling this or whatever. And also, I'm not doing any cache cleanup. So if you actually bump the version, you're probably going to run into problems because old files might still lie around. So there's definitely things that I'm just skipping over for sake of brevity and because this is already complicated enough. OK, so this is what I was saying earlier about the kind of purging of caches. You want to make sure that. Cache invalidation is hard. It is, yeah. But it is definitely worth doing. Hi, Jay. Hello. Is it because we said service worker enough times? Yes. If you say service worker three times in the mirror, I do appear. It's been really difficult at some conferences because I've just been whisked to the other side of the planet. I've just come to let you know that we've got a conference to do and there are other people who would like to talk. Fine. How close are we to the end? How close are we to the end? Actually, let me think. Where was I at? You're doing the dot thing. Oh, the templating. Yeah, it's literally two lines. Here we go. So our DOC. But how long has it been literally two more lines? Well, it would have been done faster if you didn't, you know. So we're going to take a template function. Our template takes event dot request. Go. We're going to reload. Oh, that looks OK. Look good. And now it should go back to normal. We're done. And now the test is to go offline. Really? We go offline. It still works. We can reload on all the subpages because we're offline. And the mist still doesn't work because we never went to the page. So that's fine. And I'm done. Great. Woo! That code, that code will get a lot of comments. And we'll be pushed to GitHub. And we will post the link in the Chromium desktop. And if you have questions, add the surma on Twitter at Erotrist. And you can mostly bug me because he didn't write it and he doesn't understand it. That's true. They're laughing, but they know it's true. All right. Thank you very much. Have a good one. Run across the surma.