├── ski
├── bg.png
├── objects.png
└── player.png
├── surf
├── bg.png
├── player.png
└── objects.png
├── icons
├── icon-16.png
├── icon-32.png
├── icon-114.png
├── icon-128.png
└── icon-256.png
├── register-sw.js
├── js
├── surf-error-reporting.js
├── base-error-reporting.js
├── strings.m.js
└── load_time_data.m.js
├── offline.js
├── appmanifest.json
├── .github
└── workflows
│ └── static.yml
├── README.md
├── index.html
├── favicon.svg
├── css
└── interface.css
└── sw.js
/ski/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/ski/bg.png
--------------------------------------------------------------------------------
/surf/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/surf/bg.png
--------------------------------------------------------------------------------
/ski/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/ski/objects.png
--------------------------------------------------------------------------------
/ski/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/ski/player.png
--------------------------------------------------------------------------------
/surf/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/surf/player.png
--------------------------------------------------------------------------------
/icons/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/icons/icon-16.png
--------------------------------------------------------------------------------
/icons/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/icons/icon-32.png
--------------------------------------------------------------------------------
/surf/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/surf/objects.png
--------------------------------------------------------------------------------
/icons/icon-114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/icons/icon-114.png
--------------------------------------------------------------------------------
/icons/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/icons/icon-128.png
--------------------------------------------------------------------------------
/icons/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yell0wsuit/ms-edge-letssurf/HEAD/icons/icon-256.png
--------------------------------------------------------------------------------
/register-sw.js:
--------------------------------------------------------------------------------
1 | if ('serviceWorker' in navigator) {
2 | navigator.serviceWorker.register('sw.js');
3 | }
--------------------------------------------------------------------------------
/js/surf-error-reporting.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | hookErrorReporting('webui-surf-game');
--------------------------------------------------------------------------------
/offline.js:
--------------------------------------------------------------------------------
1 | {"version":1.7,"fileList":["appmanifest.json","favicon.svg","index.html","register-sw.js","sw.js","css/interface.css","js/base-error-reporting.js","js/lib_common.chunk.js","js/lib_react.chunk.js","js/load_time_data.m.js","js/strings.m.js","js/surf.bundle.js","js/surf-error-reporting.js","surf/bg.png","surf/objects.png","surf/player.png","ski/bg.png","ski/objects.png","ski/player.png","icons/icon-114.png","icons/icon-128.png","icons/icon-16.png","icons/icon-256.png","icons/icon-32.png"]}
--------------------------------------------------------------------------------
/js/base-error-reporting.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | function hookErrorReporting(component) {
5 | window.onerror = (message, source, lineno, columnNumber, error) => {
6 | const errorInfo = {
7 | column: columnNumber,
8 | component,
9 | line: lineno,
10 | message: error.message,
11 | name: error.name,
12 | source_url: source,
13 | stack: error.stack
14 | };
15 | chrome.errorReporting.reportError(errorInfo);
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/appmanifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Let's Surf",
3 | "short_name": "Let's Surf",
4 | "description": "An offline game from Microsoft Edge",
5 | "version": "1.7",
6 | "start_url": "index.html",
7 | "display": "fullscreen",
8 | "orientation": "any",
9 | "background_color": "#ffffff",
10 | "icons": [
11 | {
12 | "src": "icons/icon-16.png",
13 | "sizes": "16x16",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "icons/icon-32.png",
18 | "sizes": "32x32",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "icons/icon-114.png",
23 | "sizes": "114x114",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "icons/icon-128.png",
28 | "sizes": "128x128",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "icons/icon-256.png",
33 | "sizes": "256x256",
34 | "type": "image/png"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v5
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v3
38 | with:
39 | # Upload entire repository
40 | path: '.'
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Microsoft Edge's *Let's Surf* + *Let's Ski*
2 | The *Let's Surf* + *Let's Ski* game from Microsoft Edge.
3 |
4 | **NEW**: Check out [Edge Surf 2](https://github.com/yell0wsuit/ms-edge-surf-2), a newly updated surf game in 2025 that features more extensive character customization, new game mode, remastered graphics, and more!
5 |
6 | *Also check out [Chrome Dino](https://github.com/yell0wsuit/chrome-dino-enhanced) that saves your highscore.*
7 |
8 |
9 |
10 |
11 |
12 | ## Play
13 | Play online at https://yell0wsuit.page/assets/games/edge-surf/index.html
14 |
15 | For offline play, go to the website and install this game as a web app (easiest method). Check the install button in the address bar of the browser.
16 | On iOS, you need to use Safari and add the page to the homescreen.
17 | On Chrome Android, a notification may appear at the bottom of the screen.
18 |
19 | ## Features
20 | - New code from Microsoft Edge v104.0.1293.47.
21 | - Added new theme, "Let's Ski" from v97. Also added a drop-down menu to change between 2 themes.
22 | - Added a new character, Linux Tux.
23 | - 3 game modes.
24 | - Keyboard, mouse, touch and controller support.
25 | - Use ``localstorage`` for storing high score instead of ``chrome.send``, which is not present for client-side.
26 | - PWA support. You can install this game as an app to play offline.
27 | - Mobile support. The game is responsive to most screen sizes.
28 | - Show touch instructions for touch screens as default.
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Let's Surf
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/js/strings.m.js:
--------------------------------------------------------------------------------
1 | import {loadTimeData} from './load_time_data.m.js';
2 | loadTimeData.data = {"bestScore":"Best $1 $2 $3","bestScoreFlyout":"Best score: $1","bestScoreMenuDisplay":"$1 $2","cancel":"Cancel","close":"Close","code":"Cheat code activated!","codeCheat":"Cheat code activated! $1","codeScoring":"Scoring turned off for this round.","congrats":"You beat $1% of global players!","endlessBestScore":-1,"endlessDescriptor":"distance","endlessModeTitleInline":"Endless:","endlessTitle":"Endless","endlessUnit":"m","fontfamily":"system-ui, sans-serif","fontsize":"75%","gameCreditsButton":"Game credits","gameCreditsDialogTitle":"Game credits","gameMode":"Game mode changed to $1","gameModeSelectLabel":"Game mode","gameModeSelectLabelDisabled":"Game mode (start new game to change)","gameSettings":"Game settings","highVisibilityActive":false,"highVisiblityModeToggleLabel":"High visibility","howToPlayButton":"How to play","howToPlayControllerBoost":"Press RT to use your speed boost","howToPlayControllerMovement":"Use the thumbstick or D-pad to steer","howToPlayControllerRefresh":"Press LB+RB to restart the game","howToPlayDialogTitle":"How to play","howToPlayEndless":"Travel as far as you can and rescue a friend for help with enemies","howToPlayKeyboardBoost":"Press `F` to use your speed boost","howToPlayKeyboardMovement":"Use the arrow or `WASD` keys to steer","howToPlayKeyboardRefresh":"Refresh the page to restart the game","howToPlayMouseBoost":"Right-click to use your speed boost","howToPlayMouseMovement":"Move the mouse to steer","howToPlayMouseRefresh":"Refresh the page to restart the game","howToPlayTimeTrial":"Reach the end as fast as you can and collect coins to subtract time","howToPlayTouchBoost":"Swipe down twice to use your speed boost","howToPlayTouchMovement":"Swipe or tap to steer","howToPlayTouchRefresh":"Refresh the page to restart the game","howToPlayZigZag":"Pass through as many gates as you can in a row","isDebugMode":false,"isLinuxTuxEnabled":true,"isNewShare":false,"is_windows_xbox_sku":"false","keyboardAction":"spacebar","language":"en","lastSelectedMode":"endless","lastSelectedPlayer":1,"lastSelectedTheme":"surf","menuInfo":"to start playing","mouseAction":"double click","newBestScore":"New best $1 $2 $3","newGame":"Start new game","off":"Off","on":"On","overInfo":"to play again","overTitle":"Nice try!","pauseInfo":"to resume playing","pauseTitle":"Game paused","reducedSpeedActive":false,"reducedSpeedModeToggleLabel":"Reduced speed","resetAllStats":"Reset all stats","resetAllStatsDialogText":"Are you sure you want to reset all your game stats, including your high scores?","resetAllStatsDialogTitle":"Reset all stats","share":"Share with a friend","shareButtonString":"Share","shareCopy":"Copy","shareLinkCopied":"Copied!","shareLinkFlyout":"https://go.microsoft.com/fwlink/?linkid=2162539","shareLinkGameOver":"https://go.microsoft.com/fwlink/?linkid=2168605","shareNewLinkMobile":"https://microsoftedgewelcome.microsoft.com/emmx/edge-surf","shareString":"I discovered a super fun game, let's surf together!","skiTheme":"Let's ski","specialThanks":"Special thanks:","startShare":"Start Share","surfTheme":"Let's surf","textdirection":"ltr","theme":"Theme changed to $1","themeSelectLabel":"Theme","timetrialBestScore":-1,"timetrialDescriptor":"time","timetrialModeTitleInline":"Time trial:","timetrialTitle":"Time trial","timetrialUnit":"s","touchAction":"double tap","zigzagBestScore":-1,"zigzagDescriptor":"streak","zigzagModeTitleInline":"Zig zag:","zigzagTitle":"Zig zag","zigzagUnit":"gates"};
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/js/load_time_data.m.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /**
6 | * @fileoverview This file defines a singleton which provides access to all data
7 | * that is available as soon as the page's resources are loaded (before DOM
8 | * content has finished loading). This data includes both localized strings and
9 | * any data that is important to have ready from a very early stage (e.g. things
10 | * that must be displayed right away).
11 | *
12 | * Note that loadTimeData is not guaranteed to be consistent between page
13 | * refreshes (https://crbug.com/740629) and should not contain values that might
14 | * change if the page is re-opened later.
15 | */
16 |
17 | /** @type {!LoadTimeData} */
18 | // eslint-disable-next-line no-var
19 | export var loadTimeData;
20 |
21 | class LoadTimeData {
22 | constructor() {
23 | /** @type {?Object} */
24 | this.data_ = null;
25 | }
26 |
27 | /**
28 | * Sets the backing object.
29 | *
30 | * Note that there is no getter for |data_| to discourage abuse of the form:
31 | *
32 | * var value = loadTimeData.data()['key'];
33 | *
34 | * @param {Object} value The de-serialized page data.
35 | */
36 | set data(value) {
37 | expect(!this.data_, 'Re-setting data.');
38 | this.data_ = value;
39 | }
40 |
41 | /**
42 | * @param {string} id An ID of a value that might exist.
43 | * @return {boolean} True if |id| is a key in the dictionary.
44 | */
45 | valueExists(id) {
46 | return this.data_ ? id in this.data_ : false;
47 | }
48 |
49 | /**
50 | * Fetches a value, expecting that it exists.
51 | * @param {string} id The key that identifies the desired value.
52 | * @return {*} The corresponding value.
53 | */
54 | getValue(id) {
55 | expect(this.data_, 'No data. Did you remember to include strings.js?');
56 | const value = this.data_[id];
57 | expect(typeof value !== 'undefined', 'Could not find value for ' + id);
58 | return value;
59 | }
60 |
61 | /**
62 | * As above, but also makes sure that the value is a string.
63 | * @param {string} id The key that identifies the desired string.
64 | * @return {string} The corresponding string value.
65 | */
66 | getString(id) {
67 | const value = this.getValue(id);
68 | expectIsType(id, value, 'string');
69 | return /** @type {string} */ (value);
70 | }
71 |
72 | /**
73 | * Returns a formatted localized string where $1 to $9 are replaced by the
74 | * second to the tenth argument.
75 | * @param {string} id The ID of the string we want.
76 | * @param {...(string|number)} var_args The extra values to include in the
77 | * formatted output.
78 | * @return {string} The formatted string.
79 | */
80 | getStringF(id, var_args) {
81 | const value = this.getString(id);
82 | if (!value) {
83 | return '';
84 | }
85 |
86 | const args = Array.prototype.slice.call(arguments);
87 | args[0] = value;
88 | return this.substituteString.apply(this, args);
89 | }
90 |
91 | /**
92 | * Returns a formatted localized string where $1 to $9 are replaced by the
93 | * second to the tenth argument. Any standalone $ signs must be escaped as
94 | * $$.
95 | * @param {string} label The label to substitute through.
96 | * This is not an resource ID.
97 | * @param {...(string|number)} var_args The extra values to include in the
98 | * formatted output.
99 | * @return {string} The formatted string.
100 | */
101 | substituteString(label, var_args) {
102 | const varArgs = arguments;
103 | return label.replace(/\$(.|$|\n)/g, function(m) {
104 | expect(m.match(/\$[$1-9]/), 'Unescaped $ found in localized string.');
105 | return m === '$$' ? '$' : varArgs[m[1]];
106 | });
107 | }
108 |
109 | /**
110 | * Returns a formatted string where $1 to $9 are replaced by the second to
111 | * tenth argument, split apart into a list of pieces describing how the
112 | * substitution was performed. Any standalone $ signs must be escaped as $$.
113 | * @param {string} label A localized string to substitute through.
114 | * This is not an resource ID.
115 | * @param {...(string|number)} var_args The extra values to include in the
116 | * formatted output.
117 | * @return {!Array} The formatted
118 | * string pieces.
119 | */
120 | getSubstitutedStringPieces(label, var_args) {
121 | const varArgs = arguments;
122 | // Split the string by separately matching all occurrences of $1-9 and of
123 | // non $1-9 pieces.
124 | const pieces = (label.match(/(\$[1-9])|(([^$]|\$([^1-9]|$))+)/g) ||
125 | []).map(function(p) {
126 | // Pieces that are not $1-9 should be returned after replacing $$
127 | // with $.
128 | if (!p.match(/^\$[1-9]$/)) {
129 | expect(
130 | (p.match(/\$/g) || []).length % 2 === 0,
131 | 'Unescaped $ found in localized string.');
132 | return {value: p.replace(/\$\$/g, '$'), arg: null};
133 | }
134 |
135 | // Otherwise, return the substitution value.
136 | return {value: varArgs[p[1]], arg: p};
137 | });
138 |
139 | return pieces;
140 | }
141 |
142 | /**
143 | * As above, but also makes sure that the value is a boolean.
144 | * @param {string} id The key that identifies the desired boolean.
145 | * @return {boolean} The corresponding boolean value.
146 | */
147 | getBoolean(id) {
148 | const value = this.getValue(id);
149 | expectIsType(id, value, 'boolean');
150 | return /** @type {boolean} */ (value);
151 | }
152 |
153 | /**
154 | * As above, but also makes sure that the value is an integer.
155 | * @param {string} id The key that identifies the desired number.
156 | * @return {number} The corresponding number value.
157 | */
158 | getInteger(id) {
159 | const value = this.getValue(id);
160 | expectIsType(id, value, 'number');
161 | expect(value === Math.floor(value), 'Number isn\'t integer: ' + value);
162 | return /** @type {number} */ (value);
163 | }
164 |
165 | /**
166 | * Override values in loadTimeData with the values found in |replacements|.
167 | * @param {Object} replacements The dictionary object of keys to replace.
168 | */
169 | overrideValues(replacements) {
170 | expect(
171 | typeof replacements === 'object',
172 | 'Replacements must be a dictionary object.');
173 | for (const key in replacements) {
174 | this.data_[key] = replacements[key];
175 | }
176 | }
177 |
178 | /**
179 | * Reset loadTimeData's data. Should only be used in tests.
180 | * @param {?Object} newData The data to restore to, when null restores to
181 | * unset state.
182 | */
183 | resetForTesting(newData = null) {
184 | this.data_ = newData;
185 | }
186 |
187 | /**
188 | * @return {boolean} Whether loadTimeData.data has been set.
189 | */
190 | isInitialized() {
191 | return this.data_ !== null;
192 | }
193 | }
194 |
195 | /**
196 | * Checks condition, throws error message if expectation fails.
197 | * @param {*} condition The condition to check for truthiness.
198 | * @param {string} message The message to display if the check fails.
199 | */
200 | function expect(condition, message) {
201 | if (!condition) {
202 | throw new Error(
203 | 'Unexpected condition on ' + document.location.href + ': ' + message);
204 | }
205 | }
206 |
207 | /**
208 | * Checks that the given value has the given type.
209 | * @param {string} id The id of the value (only used for error message).
210 | * @param {*} value The value to check the type on.
211 | * @param {string} type The type we expect |value| to be.
212 | */
213 | function expectIsType(id, value, type) {
214 | expect(
215 | typeof value === type, '[' + value + '] (' + id + ') is not a ' + type);
216 | }
217 |
218 | expect(!loadTimeData, 'should only include this file once');
219 | loadTimeData = new LoadTimeData();
220 |
221 | // Expose |loadTimeData| directly on |window|, since within a JS module the
222 | // scope is local and not all files have been updated to import the exported
223 | // |loadTimeData| explicitly.
224 | window.loadTimeData = loadTimeData;
225 |
226 |
--------------------------------------------------------------------------------
/css/interface.css:
--------------------------------------------------------------------------------
1 | /** Copyright (C) Microsoft Corporation. All rights reserved.
2 | * Use of this source code is governed by a BSD-style license that can be
3 | * found in the LICENSE file.
4 | */
5 |
6 | /* minimal css reset */
7 | html, body, div, h1, h2, h3, h4, h5, h6, a, p, img, ul, li, button { padding: 0; margin: 0; border: 0; outline: 0; }
8 |
9 | html, body {
10 | width: 100vw; /* fullscreen */
11 | height: 100vh; /* fullscreen */
12 | overflow: hidden;
13 | user-select: none;
14 | font-family: system-ui, sans-serif;
15 | font-size: 16px;
16 | color: #000000;
17 | touch-action: pinch-zoom; /* prevents navigating back with one finger */
18 | }
19 |
20 | /* game layers fill entire page */
21 | #game-canvas, #game-bg, #game-gradient, #game-ui, #game-tint {
22 | overflow: hidden;
23 | position: absolute;
24 | user-select: none;
25 | width: 100vw;
26 | height: 100vh;
27 | top: 0;
28 | left: 0;
29 | }
30 |
31 | /* from edge */
32 | #hamburger-container {
33 | overflow: visible;
34 | position: absolute;
35 | top: 10px;
36 | right: 8px;
37 | }
38 |
39 | html[dir="rtl"] #hamburger-container {
40 | right: unset;
41 | left: 10px;
42 | }
43 |
44 | html[dir="rtl"].icon-arrow-right,
45 | html[dir="rtl"].icon-arrow-left {
46 | /* undo RTL */
47 | transform: scale(-1, 1);
48 | }
49 |
50 | #modal-root {
51 | position: fixed;
52 | }
53 |
54 |
55 |
56 | /* page structure */
57 |
58 | #game-gradient {
59 | z-index: 1;
60 | pointer-events: none;
61 | }
62 |
63 | @media (forced-colors:active) {
64 | /* CanvasText, LinkText, VisitedText, ActiveText, GrayText, Field, FieldText, HighlightText, Highlight (txt bg), ButtonText, ButtonFace (btn bg) */
65 | #game-bg { background: Canvas !important; }
66 | }
67 |
68 | #game-bg {
69 | background-repeat: repeat;
70 | image-rendering: pixelated; /* important */
71 | image-rendering: crisp-edges;
72 | image-rendering: -moz-crisp-edges;
73 | image-rendering: -webkit-crisp-edges;
74 | z-index: 2;
75 | pointer-events: none;
76 | }
77 |
78 | #game-canvas {
79 | image-rendering: pixelated; /* important */
80 | image-rendering: crisp-edges;
81 | image-rendering: -moz-crisp-edges;
82 | image-rendering: -webkit-crisp-edges;
83 | z-index: 3;
84 | }
85 |
86 | /* overlay */
87 |
88 | #game-tint {
89 | pointer-events: none;
90 | z-index: 4;
91 | background: linear-gradient(180deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0.1) 100%);
92 | transition: opacity 0.35s, visibility 0s 0.35s;
93 | transition-timing-function: cubic-bezier(0.30, 0.20, 0.20, 1.00);
94 | visibility: hidden;
95 | opacity: 0;
96 | backdrop-filter: blur(0px);
97 | -webkit-backdrop-filter: blur(0px);
98 | }
99 |
100 | #game-tint.visible {
101 | pointer-events: inherit;
102 | transition: opacity 0.35s; /* instant visibility */
103 | visibility: visible;
104 | opacity: 1;
105 | backdrop-filter: blur(4px);
106 | -webkit-backdrop-filter: blur(4px);
107 | }
108 |
109 | /* main game ui */
110 |
111 | #game-ui {
112 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
113 | height: calc(100vh - 32px); /* fullscreen minus 32 padding */
114 | padding: 16px;
115 | z-index: 5;
116 | }
117 |
118 | #ui-title,
119 | #ui-subtitle {
120 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
121 | font-weight: bold;
122 | text-align: center;
123 | text-transform: uppercase;
124 | }
125 |
126 | #ui-title {
127 | margin-top: calc(10vh - 40px);
128 | line-height: 48px;
129 | font-size: 48px;
130 | }
131 |
132 | #ui-subtitle {
133 | margin-top: 10px;
134 | line-height: 24px;
135 | font-size: 24px;
136 | }
137 |
138 | /* shrinks title for super small screens */
139 | @media screen and (max-height: 480px) {
140 | #ui-title { line-height: 36px; font-size: 36px; }
141 | #ui-subtitle { line-height: 20px; font-size: 20px; }
142 | }
143 |
144 | /* hides mode title on tiny screens */
145 | @media screen and (max-height: 360px) {
146 | #ui-subtitle { visibility: hidden; }
147 | }
148 |
149 | /* hides game title on tiny screens */
150 | @media screen and (max-height: 280px) {
151 | #ui-title { visibility: hidden; }
152 | }
153 |
154 | #ui-selector {
155 | position: absolute;
156 | top: calc(40vh - 64px);
157 | left: calc(50vw - 240px);
158 | width: 480px; /* fullscreen minus 32 padding */
159 | height: 128px;
160 | display: flex;
161 | justify-content: space-between;
162 | align-items: center;
163 | }
164 |
165 | #ui-selector span {
166 | border-radius: 4px;
167 | background-color: rgba(255,255,255,0);
168 | transition: background-color 0.1s;
169 | }
170 |
171 | #ui-selector span:hover {
172 | background: rgba(255,255,255,0.4);
173 | cursor: pointer;
174 | }
175 |
176 | /* shrink menu arrows on narrow screens */
177 | @media screen and (max-width: 520px) {
178 | #ui-selector { left: calc(50vw - 220px); width: 440px; }
179 | }
180 |
181 | /* hide menu arrows on narrow screens */
182 | @media screen and (max-width: 420px) {
183 | #ui-selector span { visibility: hidden; }
184 | }
185 |
186 | #ui-container {
187 | display: inline-block;
188 | }
189 |
190 | #view-container {
191 | position: absolute;
192 | top: calc(100vh * 0.55 - 32px); /* minus half height */
193 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
194 | text-align: center;
195 | }
196 |
197 | #ui-instruct {
198 | position: relative;
199 | /* top: calc(100vh * 0.6 - 32px); /* minus half height */
200 | /* width: calc(100vw - 32px); /* fullscreen minus 32 padding */
201 | text-align: center;
202 | }
203 |
204 | #instruct-content {
205 | padding: 16px 12px;
206 | }
207 |
208 | #instruct-content.tinted {
209 | background-color: rgba(255,255,255,0.6);
210 | box-shadow: 0px 16px 24px rgba(0,0,0,0.16);
211 | border-radius: 4px;
212 | }
213 |
214 | @supports not (-moz-appearance:none) {
215 | #instruct-content.tinted {
216 | backdrop-filter: blur(32px);
217 | -webkit-backdrop-filter: blur(32px);
218 | }
219 | } /* Backdrop filter is broken on Firefox */
220 |
221 | #instruct-icon { /* xbox + ps */
222 | display: inline-block;
223 | vertical-align: middle;
224 | width: 32px;
225 | height: 32px;
226 | }
227 |
228 | #instruct-action { /* spacebar, double tap, double click */
229 | display: inline-block;
230 | margin: 0 8px;
231 | vertical-align: middle;
232 | }
233 |
234 | #instruct-action.outline { /* spacebar, double tap, double click */
235 | padding: 4px 12px 5px;
236 | border: 2px solid #000000;
237 | border-radius: 4px;
238 | font-size: 20px;
239 | font-weight: bold;
240 | text-transform: uppercase;
241 | }
242 |
243 | #instruct-text {
244 | display: inline-block;
245 | margin: 0 8px;
246 | vertical-align: middle;
247 | font-size: 24px;
248 | font-weight: 600;
249 | }
250 |
251 | #ui-notify {
252 | position: absolute;
253 | top: calc(100vh * 0.75 - 24px);
254 | width: calc(100vw - 32px); /* fullscreen minus 32 padding */
255 | text-align: center;
256 | transition: opacity 0.5s, visibility 0s 0.5s;
257 | transition-timing-function: cubic-bezier(0.30, 0.20, 0.20, 1.00);
258 | visibility: hidden;
259 | opacity: 0;
260 | }
261 |
262 | #ui-notify.visible {
263 | transition: opacity 0.35s; /* instant visibility */
264 | visibility: visible;
265 | opacity: 1;
266 | }
267 |
268 | #notify-content {
269 | display: inline-block;
270 | padding: 12px 20px;
271 | font-weight: 600;
272 | border-radius: 4px;
273 | color: #FFFFFF;
274 | background-color: rgba(0,0,0,0.7);
275 | backdrop-filter: blur(32px);
276 | -webkit-backdrop-filter: blur(32px);
277 | }
278 |
279 | #notify-shareLink {
280 | display: inline-flex;
281 | vertical-align: -33%;
282 | align-items: center;
283 | font-weight: normal;
284 | background-color: rgba(255,255,255,0.2);
285 | padding: 4px 8px 2px;
286 | margin-inline-start: 12px;
287 | border-radius: 4px;
288 | cursor: pointer;
289 | color: currentColor;
290 | }
291 |
292 | #notify-shareLink:hover {
293 | background-color: rgba(255,255,255,0.3);
294 | }
295 |
296 | #notify-shareText {
297 | }
298 |
299 | /* game stats and ui */
300 |
301 | #ui-dash {
302 | position: relative;
303 | width: 100%;
304 | margin: 0 auto;
305 | }
306 |
307 | #dash-stats {
308 | display: flex;
309 | width: 280px;
310 | margin: 0 auto;
311 | }
312 |
313 | #stats-score,
314 | .stats-icons {
315 | display: flex;
316 | justify-content: center;
317 | height: 22px;
318 | flex-basis: 136px;
319 | }
320 |
321 | .stats-icons {
322 | flex-basis: 72px;
323 | image-rendering: pixelated; /* important */
324 | image-rendering: crisp-edges;
325 | image-rendering: -moz-crisp-edges;
326 | image-rendering: -webkit-crisp-edges;
327 | }
328 |
329 | .stats-icons div {
330 | width: 22px;
331 | height: 22px;
332 | margin-inline-end: 2px;
333 | background-size: 1920px 512px;
334 | }
335 |
336 | .life-full { background-position: -1px -1px; }
337 | .life-empty { background-position: -1px -25px; }
338 | .boost-full { background-position: -25px -1px; }
339 | .boost-empty { background-position: -25px -25px; }
340 | .shield { background-position: -49px -1px; }
341 | .infinite { background-position: -49px -25px; }
342 |
343 | #score-icon {
344 | display: inline-block;
345 | vertical-align: middle;
346 | height: 22px;
347 | margin-inline-end: 4px;
348 | }
349 |
350 | #score-text {
351 | vertical-align: middle;
352 | font-size: 16px;
353 | font-weight: bold;
354 | line-height: 22px;
355 | }
356 |
357 | #ui-share {
358 | position: relative;
359 | text-align: center;
360 | margin-top: 10px;
361 | visibility: hidden;
362 | }
363 |
364 | #ui-share.visible {
365 | visibility: visible;
366 | }
367 |
368 | #share-content {
369 | padding: 12px 12px;
370 | }
371 |
372 | #share-content.tinted {
373 | background-color: rgba(255,255,255,0.6);
374 | backdrop-filter: blur(32px);
375 | -webkit-backdrop-filter: blur(32px);
376 | box-shadow: 0px 16px 24px rgba(0,0,0,0.16);
377 | border-radius: 4px;
378 | }
379 |
380 | #share-best-score {
381 | font-size: 12px;
382 | line-height: 14px;
383 | text-align: center;
384 | color: #8E8E8E;
385 | }
386 |
387 | #share-new-score {
388 | text-align: center;
389 | font-weight: 500;
390 | font-size: 30px;
391 | line-height: 32px;
392 | align-items: center;
393 | color: #000000;
394 | }
395 |
396 | #share-text {
397 | margin-top: 4px;
398 | text-align: center;
399 | font-style: normal;
400 | font-weight: 500;
401 | font-size: 14px;
402 | line-height: 16px;
403 | color: #000000;
404 | }
405 |
406 | #share-action {
407 | margin-top: 10px;
408 | width: 113px;
409 | height: 32px;
410 | background: #000000;
411 | box-sizing: border-box;
412 | border-radius: 4px;
413 | }
414 |
415 | #share-action-container {
416 | display: inline-block;
417 | margin-top: 2px;
418 | }
419 |
420 | #share-action-container > svg {
421 | vertical-align: top;
422 | }
423 |
424 | #share-action-container > svg > path {
425 | fill: #FFFFFF;
426 | }
427 |
428 | #share-action-text {
429 | font-style: normal;
430 | font-size: 18px;
431 | color: #FFFFFF;
432 | margin-left: 7.5px;
433 | font-weight: 550;
434 | }
435 |
436 | .icon-star {
437 | vertical-align: top;
438 | }
439 |
440 | .icon-fill {
441 | fill: #000000;
442 | }
443 |
444 | #ui-notify .icon-fill {
445 | fill: #FFFFFF;
446 | }
447 |
448 | @media (forced-colors:active) {
449 | /* CanvasText, LinkText, VisitedText, ActiveText, GrayText, Field, FieldText, HighlightText, Highlight (txt bg), ButtonText, ButtonFace (btn bg) */
450 | .icon-fill { fill: CanvasText !important; }
451 | }
--------------------------------------------------------------------------------
/sw.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const OFFLINE_DATA_FILE = "offline.js";
4 | const CACHE_NAME_PREFIX = "edge-surf";
5 | const BROADCASTCHANNEL_NAME = "offline";
6 | const CONSOLE_PREFIX = "[SW] ";
7 | const LAZYLOAD_KEYNAME = "";
8 |
9 | // Create a BroadcastChannel if supported.
10 | const broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(BROADCASTCHANNEL_NAME));
11 |
12 | //////////////////////////////////////
13 | // Utility methods
14 | function PostBroadcastMessage(o)
15 | {
16 | if (!broadcastChannel)
17 | return; // not supported
18 |
19 | // Impose artificial (and arbitrary!) delay of 3 seconds to make sure client is listening by the time the message is sent.
20 | // Note we could remove the delay on some messages, but then we create a race condition where sometimes messages can arrive
21 | // in the wrong order (e.g. "update ready" arrives before "started downloading update"). So to keep the consistent ordering,
22 | // delay all messages by the same amount.
23 | setTimeout(() => broadcastChannel.postMessage(o), 3000);
24 | };
25 |
26 | function Broadcast(type)
27 | {
28 | PostBroadcastMessage({
29 | "type": type
30 | });
31 | };
32 |
33 | function BroadcastDownloadingUpdate(version)
34 | {
35 | PostBroadcastMessage({
36 | "type": "downloading-update",
37 | "version": version
38 | });
39 | }
40 |
41 | function BroadcastUpdateReady(version)
42 | {
43 | PostBroadcastMessage({
44 | "type": "update-ready",
45 | "version": version
46 | });
47 | }
48 |
49 | function IsUrlInLazyLoadList(url, lazyLoadList)
50 | {
51 | if (!lazyLoadList)
52 | return false; // presumably lazy load list failed to load
53 |
54 | try {
55 | for (const lazyLoadRegex of lazyLoadList)
56 | {
57 | if (new RegExp(lazyLoadRegex).test(url))
58 | return true;
59 | }
60 | }
61 | catch (err)
62 | {
63 | console.error(CONSOLE_PREFIX + "Error matching in lazy-load list: ", err);
64 | }
65 |
66 | return false;
67 | };
68 |
69 | function WriteLazyLoadListToStorage(lazyLoadList)
70 | {
71 | if (typeof localforage === "undefined")
72 | return Promise.resolve(); // bypass if localforage not imported
73 | else
74 | return localforage.setItem(LAZYLOAD_KEYNAME, lazyLoadList)
75 | };
76 |
77 | function ReadLazyLoadListFromStorage()
78 | {
79 | if (typeof localforage === "undefined")
80 | return Promise.resolve([]); // bypass if localforage not imported
81 | else
82 | return localforage.getItem(LAZYLOAD_KEYNAME);
83 | };
84 |
85 | function GetCacheBaseName()
86 | {
87 | // Include the scope to avoid name collisions with any other SWs on the same origin.
88 | // e.g. "c3offline-https://example.com/foo/" (won't collide with anything under bar/)
89 | return CACHE_NAME_PREFIX + "-" + self.registration.scope;
90 | };
91 |
92 | function GetCacheVersionName(version)
93 | {
94 | // Append the version number to the cache name.
95 | // e.g. "c3offline-https://example.com/foo/-v2"
96 | return GetCacheBaseName() + "-v" + version;
97 | };
98 |
99 | // Return caches.keys() filtered down to just caches we're interested in (with the right base name).
100 | // This filters out caches from unrelated scopes.
101 | async function GetAvailableCacheNames()
102 | {
103 | const cacheNames = await caches.keys();
104 | const cacheBaseName = GetCacheBaseName();
105 | return cacheNames.filter(n => n.startsWith(cacheBaseName));
106 | };
107 |
108 | // Identify if an update is pending, which is the case when we have 2 or more available caches.
109 | // One must be an update that is waiting, since the next navigate that does an upgrade will
110 | // delete all the old caches leaving just one currently-in-use cache.
111 | async function IsUpdatePending()
112 | {
113 | const availableCacheNames = await GetAvailableCacheNames();
114 | return (availableCacheNames.length >= 2);
115 | };
116 |
117 | // Automatically deduce the main page URL (e.g. index.html or main.aspx) from the available browser windows.
118 | // This prevents having to hard-code an index page in the file list, implicitly caching it like AppCache did.
119 | async function GetMainPageUrl()
120 | {
121 | const allClients = await clients.matchAll({
122 | includeUncontrolled: true,
123 | type: "window"
124 | });
125 |
126 | for (const c of allClients)
127 | {
128 | // Parse off the scope from the full client URL, e.g. https://example.com/index.html -> index.html
129 | let url = c.url;
130 | if (url.startsWith(self.registration.scope))
131 | url = url.substring(self.registration.scope.length);
132 |
133 | if (url && url !== "/") // ./ is also implicitly cached so don't bother returning that
134 | {
135 | // If the URL is solely a search string, prefix it with / to ensure it caches correctly.
136 | // e.g. https://example.com/?foo=bar needs to cache as /?foo=bar, not just ?foo=bar.
137 | if (url.startsWith("?"))
138 | url = "/" + url;
139 |
140 | return url;
141 | }
142 | }
143 |
144 | return ""; // no main page URL could be identified
145 | };
146 |
147 | // Fetch optionally bypassing HTTP cache using fetch cache options
148 | function fetchWithBypass(request, bypassCache)
149 | {
150 | if (typeof request === "string")
151 | request = new Request(request);
152 |
153 | if (bypassCache)
154 | {
155 | return fetch(request.url, {
156 | method: 'GET',
157 | headers: request.headers,
158 | mode: request.mode == 'navigate' ? 'cors' : request.mode,
159 | credentials: request.credentials,
160 | redirect: request.redirect
161 | });
162 | }
163 | else
164 | {
165 | // bypass disabled: perform normal fetch which is allowed to return from HTTP cache
166 | return fetch(request);
167 | }
168 | };
169 |
170 | // Effectively a cache.addAll() that only creates the cache on all requests being successful (as a weak attempt at making it atomic)
171 | // and can optionally cache-bypass with fetchWithBypass in every request
172 | async function CreateCacheFromFileList(cacheName, fileList, bypassCache)
173 | {
174 | // Kick off all requests and wait for them all to complete
175 | const responses = await Promise.all(fileList.map(url => fetchWithBypass(url, bypassCache)));
176 |
177 | // Check if any request failed. If so don't move on to opening the cache.
178 | // This makes sure we only open a cache if all requests succeeded.
179 | let allOk = true;
180 |
181 | for (const response of responses)
182 | {
183 | if (!response.ok)
184 | {
185 | allOk = false;
186 | console.error(CONSOLE_PREFIX + "Error fetching '" + response.url + "' (" + response.status + " " + response.statusText + ")");
187 | }
188 | }
189 |
190 | if (!allOk)
191 | throw new Error("not all resources were fetched successfully");
192 |
193 | // Can now assume all responses are OK. Open a cache and write all responses there.
194 | // TODO: ideally we can do this transactionally to ensure a complete cache is written as one atomic operation.
195 | // This needs either new transactional features in the spec, or at the very least a way to rename a cache
196 | // (so we can write to a temporary name that won't be returned by GetAvailableCacheNames() and then rename it when ready).
197 | const cache = await caches.open(cacheName);
198 |
199 | try {
200 | return await Promise.all(responses.map(
201 | (response, i) => cache.put(fileList[i], response)
202 | ));
203 | }
204 | catch (err)
205 | {
206 | // Not sure why cache.put() would fail (maybe if storage quota exceeded?) but in case it does,
207 | // clean up the cache to try to avoid leaving behind an incomplete cache.
208 | console.error(CONSOLE_PREFIX + "Error writing cache entries: ", err);
209 | caches.delete(cacheName);
210 | throw err;
211 | }
212 | };
213 |
214 | async function UpdateCheck(isFirst)
215 | {
216 | try {
217 | // Always bypass cache when requesting offline.js to make sure we find out about new versions.
218 | const response = await fetchWithBypass(OFFLINE_DATA_FILE, true);
219 |
220 | if (!response.ok)
221 | throw new Error(OFFLINE_DATA_FILE + " responded with " + response.status + " " + response.statusText);
222 |
223 | const data = await response.json();
224 |
225 | const version = data.version;
226 | const fileList = data.fileList;
227 | const lazyLoadList = data.lazyLoad;
228 | const currentCacheName = GetCacheVersionName(version);
229 |
230 | const cacheExists = await caches.has(currentCacheName);
231 |
232 | // Don't recache if there is already a cache that exists for this version. Assume it is complete.
233 | if (cacheExists)
234 | {
235 | // Log whether we are up-to-date or pending an update.
236 | const isUpdatePending = await IsUpdatePending();
237 | if (isUpdatePending)
238 | {
239 | console.log(CONSOLE_PREFIX + "Update pending");
240 | Broadcast("update-pending");
241 | }
242 | else
243 | {
244 | console.log(CONSOLE_PREFIX + "Up to date");
245 | Broadcast("up-to-date");
246 | }
247 | return;
248 | }
249 |
250 | // Implicitly add the main page URL to the file list, e.g. "index.html", so we don't have to assume a specific name.
251 | const mainPageUrl = await GetMainPageUrl();
252 |
253 | // Prepend the main page URL to the file list if we found one and it is not already in the list.
254 | // Also make sure we request the base / which should serve the main page.
255 | fileList.unshift("./");
256 |
257 | if (mainPageUrl && fileList.indexOf(mainPageUrl) === -1)
258 | fileList.unshift(mainPageUrl);
259 |
260 | console.log(CONSOLE_PREFIX + "Caching " + fileList.length + " files for offline use");
261 |
262 | if (isFirst)
263 | Broadcast("downloading");
264 | else
265 | BroadcastDownloadingUpdate(version);
266 |
267 | // Note we don't bypass the cache on the first update check. This is because SW installation and the following
268 | // update check caching will race with the normal page load requests. For any normal loading fetches that have already
269 | // completed or are in-flight, it is pointless and wasteful to cache-bust the request for offline caching, since that
270 | // forces a second network request to be issued when a response from the browser HTTP cache would be fine.
271 | if (lazyLoadList)
272 | await WriteLazyLoadListToStorage(lazyLoadList); // dump lazy load list to local storage#
273 |
274 | await CreateCacheFromFileList(currentCacheName, fileList, !isFirst);
275 | const isUpdatePending = await IsUpdatePending();
276 |
277 | if (isUpdatePending)
278 | {
279 | console.log(CONSOLE_PREFIX + "All resources saved, update ready");
280 | BroadcastUpdateReady(version);
281 | }
282 | else
283 | {
284 | console.log(CONSOLE_PREFIX + "All resources saved, offline support ready");
285 | Broadcast("offline-ready");
286 | }
287 | }
288 | catch (err)
289 | {
290 | // Update check fetches fail when we're offline, but in case there's any other kind of problem with it, log a warning.
291 | console.warn(CONSOLE_PREFIX + "Update check failed: ", err);
292 | }
293 | };
294 |
295 | self.addEventListener("install", event =>
296 | {
297 | // On install kick off an update check to cache files on first use.
298 | // If it fails we can still complete the install event and leave the SW running, we'll just
299 | // retry on the next navigate.
300 | event.waitUntil(
301 | UpdateCheck(true) // first update
302 | .catch(() => null)
303 | );
304 | });
305 |
306 | async function GetCacheNameToUse(availableCacheNames, doUpdateCheck)
307 | {
308 | // Prefer the oldest cache available. This avoids mixed-version responses by ensuring that if a new cache
309 | // is created and filled due to an update check while the page is running, we keep returning resources
310 | // from the original (oldest) cache only.
311 | if (availableCacheNames.length === 1 || !doUpdateCheck)
312 | return availableCacheNames[0];
313 |
314 | // We are making a navigate request with more than one cache available. Check if we can expire any old ones.
315 | const allClients = await clients.matchAll();
316 |
317 | // If there are other clients open, don't expire anything yet. We don't want to delete any caches they
318 | // might be using, which could cause mixed-version responses.
319 | if (allClients.length > 1)
320 | return availableCacheNames[0];
321 |
322 | // Identify newest cache to use. Delete all the others.
323 | const latestCacheName = availableCacheNames[availableCacheNames.length - 1];
324 | console.log(CONSOLE_PREFIX + "Updating to new version");
325 |
326 | await Promise.all(
327 | availableCacheNames.slice(0, -1)
328 | .map(c => caches.delete(c))
329 | );
330 |
331 | return latestCacheName;
332 | };
333 |
334 | async function HandleFetch(event, doUpdateCheck)
335 | {
336 | const availableCacheNames = await GetAvailableCacheNames();
337 |
338 | // No caches available: go to network
339 | if (!availableCacheNames.length)
340 | return fetch(event.request);
341 |
342 | const useCacheName = await GetCacheNameToUse(availableCacheNames, doUpdateCheck);
343 | const cache = await caches.open(useCacheName);
344 | const cachedResponse = await cache.match(event.request);
345 |
346 | if (cachedResponse)
347 | return cachedResponse; // use cached response
348 |
349 | // We need to check if this request is to be lazy-cached. Send the request and load the lazy-load list
350 | // from storage simultaneously.
351 | const result = await Promise.all([fetch(event.request), ReadLazyLoadListFromStorage()]);
352 | const fetchResponse = result[0];
353 | const lazyLoadList = result[1];
354 |
355 | if (IsUrlInLazyLoadList(event.request.url, lazyLoadList))
356 | {
357 | // Handle failure writing to the cache. This can happen if the storage quota is exceeded, which is particularly
358 | // likely in Safari 11.1, which appears to have very tight storage limits. Make sure even in the event of an error
359 | // we continue to return the response from the fetch.
360 | try {
361 | // Note clone response since we also respond with it
362 | await cache.put(event.request, fetchResponse.clone());
363 | }
364 | catch (err)
365 | {
366 | console.warn(CONSOLE_PREFIX + "Error caching '" + event.request.url + "': ", err);
367 | }
368 | }
369 |
370 | return fetchResponse;
371 | };
372 |
373 | self.addEventListener("fetch", event =>
374 | {
375 | /** NOTE (iain)
376 | * This check is to prevent a bug with XMLHttpRequest where if its
377 | * proxied with "FetchEvent.prototype.respondWith" no upload progress
378 | * events are triggered. By returning we allow the default action to
379 | * occur instead. Currently all cross-origin requests fall back to default.
380 | */
381 | if (new URL(event.request.url).origin !== location.origin)
382 | return;
383 |
384 | // Check for an update on navigate requests
385 | const doUpdateCheck = (event.request.mode === "navigate");
386 |
387 | const responsePromise = HandleFetch(event, doUpdateCheck);
388 |
389 | if (doUpdateCheck)
390 | {
391 | // allow the main request to complete, then check for updates
392 | event.waitUntil(
393 | responsePromise
394 | .then(() => UpdateCheck(false)) // not first check
395 | );
396 | }
397 |
398 | event.respondWith(responsePromise);
399 | });
--------------------------------------------------------------------------------