Repository: maxence-charriere/go-app (language: go)

Issue:
go-app v10 does not work in MacOS Safari
I'm trying to upgrade go-app from v9 to v10 -> https://github.com/rtrzebinski/simple-memorizer-4/tree/go-app-v10

go-app v10 does not work in MacOS Safari browser

It works fine in Google Chrome.

I checked on 2 (Mac) computers - same result.

I use incognito mode to ensure it's no caching issue.

Error displayed in Safari -> `TypeError: TypeError: Load failed`.

In console I see it's related to this part:

`loading wasm failed: TypeError: TypeError: Load failed`

```js
  try {
    const showProgress = (progress) => {
      loaderLabel.innerText = goappLoadingLabel.replace("{progress}", progress);
    };
    showProgress(0);

    const go = new Go();
    const wasm = await instantiateStreaming(
      fetchWithProgress("/web/app.wasm", showProgress),
      go.importObject
    );

    go.run(wasm.instance);
    loader.remove();
  } catch (err) {
    loaderIcon.className = "goapp-logo";
    loaderLabel.innerText = err;
    console.error("loading wasm failed: ", err);  // <<<--- error is printed here "loading wasm failed: TypeError: TypeError: Load failed"
  }
}
```

The file exists:

```bash
$ ls web/app.wasm 
web/app.wasm
```

Anyone has this problem before? Thanks in advance for any help.


Discussion / hints:
We also test with Safari on macOS and it works fine. I think there should be more information using the developer tools inside of Safari. Do you have a location, where I can check your version?
Hi @oderwat 

You can replicate by checking out the branch `go-app-v10` -> https://github.com/rtrzebinski/simple-memorizer-4/pull/6

Then:

```
make dev
make run
```

I know you run this app before but for the reference dependencies are here -> https://github.com/rtrzebinski/simple-memorizer-4/wiki/Dev-documentation#prerequisites
Just some comments:

* `docker-compose` is old, and you probably should switch to use `docker compose` (compose v2 plugin). I just removed the `-` everywhere.
* After that switch you should remove the `version` in the compose files.
* Waiting for the DB failed for me as it was slower that a second. We wrote some internal tool to do this which handles services, ports, docker, log file content and a lot more. But you can do that like follows:

```
dev: ## Prepare local dev environment (stop + start + migrate + seed)
	@echo "$(OK_COLOR)==> Prepare dev environment for $(SERVICE_NAME)... $(NO_COLOR)"
	@make stop
	@make start
	@echo "$(OK_COLOR)==> Waiting for the db to be ready... $(NO_COLOR)"
	@until docker exec dev-sm-db-1 pg_isready 2>&1 | grep -q "accepting connections"; do \
          sleep 0.1; \
         done
	@make migrate
	@make seed
	@echo "$(OK_COLOR)==> Completed $(NO_COLOR)"
```

Besides that it runs flawlessly in Safari on my (Intel) iMac - Sonoma 14.6.1 on "normal" mode, but fails with http://localhost in private browsing mode. I think this is the result of some security "feature" but I could not find a quick source for that. The error Safari gives for loading the WASM is "Die Ressource wurde auf nicht sichere Weise angefordert.". This is after a 200 response in the network tab.

Using a warp / reverse proxy that makes it an HTTPS connection makes it work. It then also worked on iPhone (iOS) when I was using this HTTPS "warped" setup.

In the past I made a "standard" server for our PWA development that was using SSL with a local certificate because I was aware of the problems with not having HTTPS and that production is always only on HTTPS.

I think you do not need to use "private" mode and could instead switch of the caching. Anyway, running the PWA setup without HTTPS is not really enough to test stuff.

@oderwat Thank you for all the tips! I'll apply them, but later in another PR, to keep this branch focused on the go-app v10 upgrade (so it can be useful for others at a reference).

In Safari in normal mode I still had the `TypeError: import gojs:runtime.scheduleTimeoutEvent must be an object` -> same error I reported in https://github.com/maxence-charriere/go-app/issues/989.

That made me think that my browser must have some left overs cached from go-app v9.

I checked local storage - empty.

Then I clicked `Develop -> Empty Caches` and.. everything works :D 

Thank you so much!
Sadly I need to re-open this one.

I noticed that Safari will never fetch updated application until `Develop -> Empty Caches` is clicked.

(on Chrome it works fine)

Steps to reproduce:

Build and run version A -> `make run`.

Check version on a main page - for instance `Tue Sep 3 17:43:40 CEST 2024`.

Stop the process with `ctrl+c`.

Build and run version B -> `make run`.

Check version on a main page - it will stay the same `Tue Sep 3 17:43:40 CEST 2024`.

Does not matter for how long I refresh the old version will be cached.

Only `Develop -> Empty Caches` will force new version to be downloaded.

In Chrome when I refresh the page version B is loaded.

This was never a case in v9, only started since v10.

@oderwat can you please try to replicate on your machine?
I think this is related to the removal of the automatic update checker in V10. As long as you do not implement the server update checks (see my diff, something with the update interval) you will not get updates. Refreshing the page actually is not supposed to (always) update the app. This is a misconception on how a PWA is supposed to work. Besides that, there are quite some quirks with PWAs in "Apple Products". I think Apple actively tries to hold them "at bay", because they harm their Apple Store sales and put a burden on their systems (push notifications) without them getting monetary compensations for running the infrastructure. They are also very conservative with checking for new versions, even if you use everything you "should" do. I fear there is not a lot that Go-App can do about that. I asked @maxence-charriere already if there could be support for a non PWA WASM setup could be added though.
This is sad.

I personally don't really care about PWA, just need a way to build a FE with GO only and without any JS at all.

@oderwat  I tried to "borrow" your solution -> https://github.com/rtrzebinski/simple-memorizer-4/pull/6/commits/9f8a45272b334e6aac5e70e0da333f2eb191f2b7

I see in logs it checks for update, but when I refresh the page a new version is not being loaded, I must be missing something.
> I personally don't really care about PWA, just need a way to build a FE with GO only and without any JS at all.

Well you could check out https://github.com/beego/bee, https://github.com/donseba/go-htmx or even https://www.cogentcore.org/core/
Try compiling only the go-app server and then run it to see if it can be updated. Perhaps it is because the version  of BE is older than that of FE.
I don't think it will make a difference but I made a minor tweak here:
```sh
go get -u github.com/maxence-charriere/go-app/v10@0fcf1a1
```

Could you try it?
Safari behaves oddly when I'm working on localhost, so I avoid using it for development. My live service (https://murlok.io/) generally works well on Safari and updates as expected. Occasionally, it doesn't detect an update, but a reload usually resolves the issue.
@maxence-charriere Thank you, I tried your [commit](https://github.com/maxence-charriere/go-app/commit/0fcf1a1) and it changed the version but I needed to refresh around 50+ times and it only happened after a long time (a minute at least).

While this is ok for production, it's not really usable for dev.

Would that be possible to have an instant(ish) reload of dev? Or at least so it works same way as in v9 (it was ok).

@oderwat Thank you for suggestions. I invested so much time in go-app that I don't really want to abandon it, also despite many issues I generally like the syntax and how it works. 

@gepengscu Thank you. I believe I compile both FE and BE together so they should be same version. Not sure how to separate it and whether that would help.
Hi @rtrzebinski , I gave another try to a fix:

```sh
go get -u github.com/maxence-charriere/go-app/v10@e4002e9
```

Could you try it and let me know how it goes?
> Hi @rtrzebinski , I gave another try to a fix:
> 
> ```shell
> go get -u github.com/maxence-charriere/go-app/v10@e4002e9
> ```
> 
> Could you try it and let me know how it goes?

Hi @maxence-charriere 

I tried that version.

Safari -> After `Empty Caches` and reloading it always gives me errors:

```
FetchEvent.respondWith received an error: Load failed

Failed to load resource: FetchEvent.respondWith received an error: Load failed

loading wasm failed: 

TypeError: TypeError: FetchEvent.respondWith received an error: Load failed
```

Chrome -> Works fine, new version is loaded quickly (maybe few seconds delay max)

Fix patch:
diff --git a/pkg/analytics/google-analytics.go b/pkg/analytics/google-analytics.go
index efe194f8..82851462 100644
--- a/pkg/analytics/google-analytics.go
+++ b/pkg/analytics/google-analytics.go
@@ -10,7 +10,7 @@ import (
 // field to initialize Google Analytics.
 func GoogleAnalyticsHeader(propertyID string) string {
 	return fmt.Sprintf(`<!-- Global site tag (gtag.js) - Google Analytics -->
-	<script async src="https://www.googletagmanager.com/gtag/js?id=%s"></script>
+	<script defer src="https://www.googletagmanager.com/gtag/js?id=%s"></script>
 	<script>
 	  window.dataLayer = window.dataLayer || [];
 	  function gtag(){dataLayer.push(arguments);}
diff --git a/pkg/app/engine.go b/pkg/app/engine.go
index a6b461cc..2e89765e 100644
--- a/pkg/app/engine.go
+++ b/pkg/app/engine.go
@@ -340,7 +340,7 @@ func (e *engineX) Encode(w *bytes.Buffer, document HTMLHtml) error {
 	children = append(children, body.body()...)
 	body.setBody(children)
 
-	w.WriteString("<!DOCTYPE html>\n")
+	w.WriteString("<!doctype html>\n")
 	e.nodes.Encode(e.baseContext(), w, document)
 	return nil
 }
diff --git a/pkg/app/gen/app-worker.js b/pkg/app/gen/app-worker.js
index 5dbcb456..57adde5f 100644
--- a/pkg/app/gen/app-worker.js
+++ b/pkg/app/gen/app-worker.js
@@ -4,28 +4,41 @@
 const cacheName = "app-" + "{{.Version}}";
 const resourcesToCache = {{.ResourcesToCache}};
 
-self.addEventListener("install", (event) => {
-  console.log("installing app worker {{.Version}}");
-  event.waitUntil(installWorker());
+self.addEventListener("install", async (event) => {
+  try {
+    console.log("installing app worker {{.Version}}");
+    await installWorker();
+    await self.skipWaiting();
+  } catch (error) {
+    console.error("error during installation:", error);
+  }
 });
 
 async function installWorker() {
   const cache = await caches.open(cacheName);
   await cache.addAll(resourcesToCache);
-  await self.skipWaiting(); // Use this new service worker
 }
 
-self.addEventListener("activate", (event) => {
-  event.waitUntil(deletePreviousCaches());
-  console.log("app worker {{.Version}} is activated");
+self.addEventListener("activate", async (event) => {
+  try {
+    await deletePreviousCaches(); // Await cache cleanup
+    await self.clients.claim(); // Ensure the service worker takes control of the clients
+    console.log("app worker {{.Version}} is activated");
+  } catch (error) {
+    console.error("error during activation:", error);
+  }
 });
 
 async function deletePreviousCaches() {
   keys = await caches.keys();
   keys.forEach(async (key) => {
     if (key != cacheName) {
-      console.log("deleting", key, "cache");
-      await caches.delete(key);
+      try {
+        console.log("deleting", key, "cache");
+        await caches.delete(key);
+      } catch (err) {
+        console.error("deleting", key, "cache failed:", err);
+      }
     }
   });
 }
@@ -35,11 +48,11 @@ self.addEventListener("fetch", (event) => {
 });
 
 async function fetchWithCache(request) {
-  cachedResponse = await caches.match(request);
+  const cachedResponse = await caches.match(request);
   if (cachedResponse) {
     return cachedResponse;
   }
-  return fetch(request);
+  return await fetch(request);
 }
 
 // -----------------------------------------------------------------------------
diff --git a/pkg/app/gen/app.js b/pkg/app/gen/app.js
index d034fd63..6ba8ed5c 100644
--- a/pkg/app/gen/app.js
+++ b/pkg/app/gen/app.js
@@ -31,18 +31,16 @@ goappInitWebAssembly();
 // -----------------------------------------------------------------------------
 async function goappInitServiceWorker() {
   if ("serviceWorker" in navigator) {
-    window.addEventListener("load", async () => {
-      try {
-        const registration = await navigator.serviceWorker.register(
-          "{{.WorkerJS}}"
-        );
-        goappServiceWorkerRegistration = registration;
-        goappSetupNotifyUpdate(registration);
-        goappSetupPushNotification();
-      } catch (err) {
-        console.error("goapp service worker registration failed: ", err);
-      }
-    });
+    try {
+      const registration = await navigator.serviceWorker.register(
+        "{{.WorkerJS}}"
+      );
+      goappServiceWorkerRegistration = registration;
+      goappSetupNotifyUpdate(registration);
+      goappSetupPushNotification();
+    } catch (err) {
+      console.error("goapp service worker registration failed: ", err);
+    }
   }
 }
 
@@ -64,10 +62,11 @@ function goappSetupNotifyUpdate(registration) {
       if (!navigator.serviceWorker.controller) {
         return;
       }
-      if (newSW.state != "activated") {
-        return;
+
+      switch (newSW.state) {
+        case "installed":
+          goappOnUpdate();
       }
-      goappOnUpdate();
     });
   });
 }
diff --git a/pkg/app/http.go b/pkg/app/http.go
index d6c21dc4..f45dd3a6 100644
--- a/pkg/app/http.go
+++ b/pkg/app/http.go
@@ -347,12 +347,12 @@ func (h *Handler) makeAppWorkerJS() []byte {
 		"/app.js",
 		"/manifest.webmanifest",
 		"/wasm_exec.js",
-		"/",
 		"/web/app.wasm",
+		"/",
 	)
+	setResources(h.Fonts...)
 	setResources(h.Icon.Default, h.Icon.Large, h.Icon.Maskable)
 	setResources(h.Styles...)
-	setResources(h.Fonts...)
 	setResources(h.Scripts...)
 	setResources(h.CacheableResources...)
 
@@ -361,7 +361,7 @@ func (h *Handler) makeAppWorkerJS() []byte {
 		resourcesTocache = append(resourcesTocache, h.Resources.Resolve(k))
 	}
 	sort.Slice(resourcesTocache, func(a, b int) bool {
-		return strings.Compare(resourcesTocache[a], resourcesTocache[b]) < 0
+		return strings.Compare(resourcesTocache[a], resourcesTocache[b]) > 0
 	})
 
 	var b bytes.Buffer
@@ -647,7 +647,10 @@ func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) {
 					Content(h.ThemeColor),
 				Meta().
 					Name("viewport").
-					Content("width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
+					Content("width=device-width, initial-scale=1, viewport-fit=cover"),
+				Meta().
+					Name("apple-mobile-web-app-capable").
+					Content("yes"),
 				Meta().
 					Property("og:url").
 					Content(resolveOGResource(h.Domain, h.Resources.Resolve(page.URL().Path))),
@@ -706,15 +709,6 @@ func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) {
 					}
 					return nil
 				}),
-				Range(h.Styles).Slice(func(i int) UI {
-					if resource := parseHTTPResource(h.Styles[i]); resource.URL != "" {
-						return resource.toLink().
-							Type("text/css").
-							Rel("preload").
-							As("style")
-					}
-					return nil
-				}),
 				Link().
 					Rel("icon").
 					Href(icon),
diff --git a/pkg/app/scripts.go b/pkg/app/scripts.go
index 08adcad3..0d7de09e 100644
--- a/pkg/app/scripts.go
+++ b/pkg/app/scripts.go
@@ -6,11 +6,11 @@ package app
 
 const (
 	// The default template used to generate app-worker.js.
-	DefaultAppWorkerJS = "// -----------------------------------------------------------------------------\n// PWA\n// -----------------------------------------------------------------------------\nconst cacheName = \"app-\" + \"{{.Version}}\";\nconst resourcesToCache = {{.ResourcesToCache}};\n\nself.addEventListener(\"install\", (event) => {\n  console.log(\"installing app worker {{.Version}}\");\n  event.waitUntil(installWorker());\n});\n\nasync function installWorker() {\n  const cache = await caches.open(cacheName);\n  await cache.addAll(resourcesToCache);\n  await self.skipWaiting(); // Use this new service worker\n}\n\nself.addEventListener(\"activate\", (event) => {\n  event.waitUntil(deletePreviousCaches());\n  console.log(\"app worker {{.Version}} is activated\");\n});\n\nasync function deletePreviousCaches() {\n  keys = await caches.keys();\n  keys.forEach(async (key) => {\n    if (key != cacheName) {\n      console.log(\"deleting\", key, \"cache\");\n      await caches.delete(key);\n    }\n  });\n}\n\nself.addEventListener(\"fetch\", (event) => {\n  event.respondWith(fetchWithCache(event.request));\n});\n\nasync function fetchWithCache(request) {\n  cachedResponse = await caches.match(request);\n  if (cachedResponse) {\n    return cachedResponse;\n  }\n  return fetch(request);\n}\n\n// -----------------------------------------------------------------------------\n// Push Notifications\n// -----------------------------------------------------------------------------\nself.addEventListener(\"push\", (event) => {\n  if (!event.data || !event.data.text()) {\n    return;\n  }\n\n  const notification = JSON.parse(event.data.text());\n  if (!notification) {\n    return;\n  }\n\n  const title = notification.title;\n  delete notification.title;\n\n  if (!notification.data) {\n    notification.data = {};\n  }\n  let actions = [];\n  for (let i in notification.actions) {\n    const action = notification.actions[i];\n\n    actions.push({\n      action: action.action,\n      path: action.path,\n    });\n\n    delete action.path;\n  }\n  notification.data.goapp = {\n    path: notification.path,\n    actions: actions,\n  };\n  delete notification.path;\n\n  event.waitUntil(self.registration.showNotification(title, notification));\n});\n\nself.addEventListener(\"notificationclick\", (event) => {\n  event.notification.close();\n\n  const notification = event.notification;\n  let path = notification.data.goapp.path;\n\n  for (let i in notification.data.goapp.actions) {\n    const action = notification.data.goapp.actions[i];\n    if (action.action === event.action) {\n      path = action.path;\n      break;\n    }\n  }\n\n  event.waitUntil(\n    clients\n      .matchAll({\n        type: \"window\",\n      })\n      .then((clientList) => {\n        for (var i = 0; i < clientList.length; i++) {\n          let client = clientList[i];\n          if (\"focus\" in client) {\n            client.focus();\n            client.postMessage({\n              goapp: {\n                type: \"notification\",\n                path: path,\n              },\n            });\n            return;\n          }\n        }\n\n        if (clients.openWindow) {\n          return clients.openWindow(path);\n        }\n      })\n  );\n});\n"
+	DefaultAppWorkerJS = "// -----------------------------------------------------------------------------\n// PWA\n// -----------------------------------------------------------------------------\nconst cacheName = \"app-\" + \"{{.Version}}\";\nconst resourcesToCache = {{.ResourcesToCache}};\n\nself.addEventListener(\"install\", async (event) => {\n  try {\n    console.log(\"installing app worker {{.Version}}\");\n    await installWorker();\n    await self.skipWaiting();\n  } catch (error) {\n    console.error(\"error during installation:\", error);\n  }\n});\n\nasync function installWorker() {\n  const cache = await caches.open(cacheName);\n  await cache.addAll(resourcesToCache);\n}\n\nself.addEventListener(\"activate\", async (event) => {\n  try {\n    await deletePreviousCaches(); // Await cache cleanup\n    await self.clients.claim(); // Ensure the service worker takes control of the clients\n    console.log(\"app worker {{.Version}} is activated\");\n  } catch (error) {\n    console.error(\"error during activation:\", error);\n  }\n});\n\nasync function deletePreviousCaches() {\n  keys = await caches.keys();\n  keys.forEach(async (key) => {\n    if (key != cacheName) {\n      try {\n        console.log(\"deleting\", key, \"cache\");\n        await caches.delete(key);\n      } catch (err) {\n        console.error(\"deleting\", key, \"cache failed:\", err);\n      }\n    }\n  });\n}\n\nself.addEventListener(\"fetch\", (event) => {\n  event.respondWith(fetchWithCache(event.request));\n});\n\nasync function fetchWithCache(request) {\n  const cachedResponse = await caches.match(request);\n  if (cachedResponse) {\n    return cachedResponse;\n  }\n  return await fetch(request);\n}\n\n// -----------------------------------------------------------------------------\n// Push Notifications\n// -----------------------------------------------------------------------------\nself.addEventListener(\"push\", (event) => {\n  if (!event.data || !event.data.text()) {\n    return;\n  }\n\n  const notification = JSON.parse(event.data.text());\n  if (!notification) {\n    return;\n  }\n\n  const title = notification.title;\n  delete notification.title;\n\n  if (!notification.data) {\n    notification.data = {};\n  }\n  let actions = [];\n  for (let i in notification.actions) {\n    const action = notification.actions[i];\n\n    actions.push({\n      action: action.action,\n      path: action.path,\n    });\n\n    delete action.path;\n  }\n  notification.data.goapp = {\n    path: notification.path,\n    actions: actions,\n  };\n  delete notification.path;\n\n  event.waitUntil(self.registration.showNotification(title, notification));\n});\n\nself.addEventListener(\"notificationclick\", (event) => {\n  event.notification.close();\n\n  const notification = event.notification;\n  let path = notification.data.goapp.path;\n\n  for (let i in notification.data.goapp.actions) {\n    const action = notification.data.goapp.actions[i];\n    if (action.action === event.action) {\n      path = action.path;\n      break;\n    }\n  }\n\n  event.waitUntil(\n    clients\n      .matchAll({\n        type: \"window\",\n      })\n      .then((clientList) => {\n        for (var i = 0; i < clientList.length; i++) {\n          let client = clientList[i];\n          if (\"focus\" in client) {\n            client.focus();\n            client.postMessage({\n              goapp: {\n                type: \"notification\",\n                path: path,\n              },\n            });\n            return;\n          }\n        }\n\n        if (clients.openWindow) {\n          return clients.openWindow(path);\n        }\n      })\n  );\n});\n"
 
 	wasmExecJSGoCurrent = "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n\"use strict\";\n\n(() => {\n\tconst enosys = () => {\n\t\tconst err = new Error(\"not implemented\");\n\t\terr.code = \"ENOSYS\";\n\t\treturn err;\n\t};\n\n\tif (!globalThis.fs) {\n\t\tlet outputBuf = \"\";\n\t\tglobalThis.fs = {\n\t\t\tconstants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused\n\t\t\twriteSync(fd, buf) {\n\t\t\t\toutputBuf += decoder.decode(buf);\n\t\t\t\tconst nl = outputBuf.lastIndexOf(\"\\n\");\n\t\t\t\tif (nl != -1) {\n\t\t\t\t\tconsole.log(outputBuf.substring(0, nl));\n\t\t\t\t\toutputBuf = outputBuf.substring(nl + 1);\n\t\t\t\t}\n\t\t\t\treturn buf.length;\n\t\t\t},\n\t\t\twrite(fd, buf, offset, length, position, callback) {\n\t\t\t\tif (offset !== 0 || length !== buf.length || position !== null) {\n\t\t\t\t\tcallback(enosys());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst n = this.writeSync(fd, buf);\n\t\t\t\tcallback(null, n);\n\t\t\t},\n\t\t\tchmod(path, mode, callback) { callback(enosys()); },\n\t\t\tchown(path, uid, gid, callback) { callback(enosys()); },\n\t\t\tclose(fd, callback) { callback(enosys()); },\n\t\t\tfchmod(fd, mode, callback) { callback(enosys()); },\n\t\t\tfchown(fd, uid, gid, callback) { callback(enosys()); },\n\t\t\tfstat(fd, callback) { callback(enosys()); },\n\t\t\tfsync(fd, callback) { callback(null); },\n\t\t\tftruncate(fd, length, callback) { callback(enosys()); },\n\t\t\tlchown(path, uid, gid, callback) { callback(enosys()); },\n\t\t\tlink(path, link, callback) { callback(enosys()); },\n\t\t\tlstat(path, callback) { callback(enosys()); },\n\t\t\tmkdir(path, perm, callback) { callback(enosys()); },\n\t\t\topen(path, flags, mode, callback) { callback(enosys()); },\n\t\t\tread(fd, buffer, offset, length, position, callback) { callback(enosys()); },\n\t\t\treaddir(path, callback) { callback(enosys()); },\n\t\t\treadlink(path, callback) { callback(enosys()); },\n\t\t\trename(from, to, callback) { callback(enosys()); },\n\t\t\trmdir(path, callback) { callback(enosys()); },\n\t\t\tstat(path, callback) { callback(enosys()); },\n\t\t\tsymlink(path, link, callback) { callback(enosys()); },\n\t\t\ttruncate(path, length, callback) { callback(enosys()); },\n\t\t\tunlink(path, callback) { callback(enosys()); },\n\t\t\tutimes(path, atime, mtime, callback) { callback(enosys()); },\n\t\t};\n\t}\n\n\tif (!globalThis.process) {\n\t\tglobalThis.process = {\n\t\t\tgetuid() { return -1; },\n\t\t\tgetgid() { return -1; },\n\t\t\tgeteuid() { return -1; },\n\t\t\tgetegid() { return -1; },\n\t\t\tgetgroups() { throw enosys(); },\n\t\t\tpid: -1,\n\t\t\tppid: -1,\n\t\t\tumask() { throw enosys(); },\n\t\t\tcwd() { throw enosys(); },\n\t\t\tchdir() { throw enosys(); },\n\t\t}\n\t}\n\n\tif (!globalThis.crypto) {\n\t\tthrow new Error(\"globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)\");\n\t}\n\n\tif (!globalThis.performance) {\n\t\tthrow new Error(\"globalThis.performance is not available, polyfill required (performance.now only)\");\n\t}\n\n\tif (!globalThis.TextEncoder) {\n\t\tthrow new Error(\"globalThis.TextEncoder is not available, polyfill required\");\n\t}\n\n\tif (!globalThis.TextDecoder) {\n\t\tthrow new Error(\"globalThis.TextDecoder is not available, polyfill required\");\n\t}\n\n\tconst encoder = new TextEncoder(\"utf-8\");\n\tconst decoder = new TextDecoder(\"utf-8\");\n\n\tglobalThis.Go = class {\n\t\tconstructor() {\n\t\t\tthis.argv = [\"js\"];\n\t\t\tthis.env = {};\n\t\t\tthis.exit = (code) => {\n\t\t\t\tif (code !== 0) {\n\t\t\t\t\tconsole.warn(\"exit code:\", code);\n\t\t\t\t}\n\t\t\t};\n\t\t\tthis._exitPromise = new Promise((resolve) => {\n\t\t\t\tthis._resolveExitPromise = resolve;\n\t\t\t});\n\t\t\tthis._pendingEvent = null;\n\t\t\tthis._scheduledTimeouts = new Map();\n\t\t\tthis._nextCallbackTimeoutID = 1;\n\n\t\t\tconst setInt64 = (addr, v) => {\n\t\t\t\tthis.mem.setUint32(addr + 0, v, true);\n\t\t\t\tthis.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);\n\t\t\t}\n\n\t\t\tconst setInt32 = (addr, v) => {\n\t\t\t\tthis.mem.setUint32(addr + 0, v, true);\n\t\t\t}\n\n\t\t\tconst getInt64 = (addr) => {\n\t\t\t\tconst low = this.mem.getUint32(addr + 0, true);\n\t\t\t\tconst high = this.mem.getInt32(addr + 4, true);\n\t\t\t\treturn low + high * 4294967296;\n\t\t\t}\n\n\t\t\tconst loadValue = (addr) => {\n\t\t\t\tconst f = this.mem.getFloat64(addr, true);\n\t\t\t\tif (f === 0) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tif (!isNaN(f)) {\n\t\t\t\t\treturn f;\n\t\t\t\t}\n\n\t\t\t\tconst id = this.mem.getUint32(addr, true);\n\t\t\t\treturn this._values[id];\n\t\t\t}\n\n\t\t\tconst storeValue = (addr, v) => {\n\t\t\t\tconst nanHead = 0x7FF80000;\n\n\t\t\t\tif (typeof v === \"number\" && v !== 0) {\n\t\t\t\t\tif (isNaN(v)) {\n\t\t\t\t\t\tthis.mem.setUint32(addr + 4, nanHead, true);\n\t\t\t\t\t\tthis.mem.setUint32(addr, 0, true);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.mem.setFloat64(addr, v, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (v === undefined) {\n\t\t\t\t\tthis.mem.setFloat64(addr, 0, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet id = this._ids.get(v);\n\t\t\t\tif (id === undefined) {\n\t\t\t\t\tid = this._idPool.pop();\n\t\t\t\t\tif (id === undefined) {\n\t\t\t\t\t\tid = this._values.length;\n\t\t\t\t\t}\n\t\t\t\t\tthis._values[id] = v;\n\t\t\t\t\tthis._goRefCounts[id] = 0;\n\t\t\t\t\tthis._ids.set(v, id);\n\t\t\t\t}\n\t\t\t\tthis._goRefCounts[id]++;\n\t\t\t\tlet typeFlag = 0;\n\t\t\t\tswitch (typeof v) {\n\t\t\t\t\tcase \"object\":\n\t\t\t\t\t\tif (v !== null) {\n\t\t\t\t\t\t\ttypeFlag = 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"string\":\n\t\t\t\t\t\ttypeFlag = 2;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"symbol\":\n\t\t\t\t\t\ttypeFlag = 3;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"function\":\n\t\t\t\t\t\ttypeFlag = 4;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthis.mem.setUint32(addr + 4, nanHead | typeFlag, true);\n\t\t\t\tthis.mem.setUint32(addr, id, true);\n\t\t\t}\n\n\t\t\tconst loadSlice = (addr) => {\n\t\t\t\tconst array = getInt64(addr + 0);\n\t\t\t\tconst len = getInt64(addr + 8);\n\t\t\t\treturn new Uint8Array(this._inst.exports.mem.buffer, array, len);\n\t\t\t}\n\n\t\t\tconst loadSliceOfValues = (addr) => {\n\t\t\t\tconst array = getInt64(addr + 0);\n\t\t\t\tconst len = getInt64(addr + 8);\n\t\t\t\tconst a = new Array(len);\n\t\t\t\tfor (let i = 0; i < len; i++) {\n\t\t\t\t\ta[i] = loadValue(array + i * 8);\n\t\t\t\t}\n\t\t\t\treturn a;\n\t\t\t}\n\n\t\t\tconst loadString = (addr) => {\n\t\t\t\tconst saddr = getInt64(addr + 0);\n\t\t\t\tconst len = getInt64(addr + 8);\n\t\t\t\treturn decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));\n\t\t\t}\n\n\t\t\tconst timeOrigin = Date.now() - performance.now();\n\t\t\tthis.importObject = {\n\t\t\t\t_gotest: {\n\t\t\t\t\tadd: (a, b) => a + b,\n\t\t\t\t},\n\t\t\t\tgojs: {\n\t\t\t\t\t// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)\n\t\t\t\t\t// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported\n\t\t\t\t\t// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).\n\t\t\t\t\t// This changes the SP, thus we have to update the SP used by the imported function.\n\n\t\t\t\t\t// func wasmExit(code int32)\n\t\t\t\t\t\"runtime.wasmExit\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst code = this.mem.getInt32(sp + 8, true);\n\t\t\t\t\t\tthis.exited = true;\n\t\t\t\t\t\tdelete this._inst;\n\t\t\t\t\t\tdelete this._values;\n\t\t\t\t\t\tdelete this._goRefCounts;\n\t\t\t\t\t\tdelete this._ids;\n\t\t\t\t\t\tdelete this._idPool;\n\t\t\t\t\t\tthis.exit(code);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)\n\t\t\t\t\t\"runtime.wasmWrite\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst fd = getInt64(sp + 8);\n\t\t\t\t\t\tconst p = getInt64(sp + 16);\n\t\t\t\t\t\tconst n = this.mem.getInt32(sp + 24, true);\n\t\t\t\t\t\tfs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func resetMemoryDataView()\n\t\t\t\t\t\"runtime.resetMemoryDataView\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tthis.mem = new DataView(this._inst.exports.mem.buffer);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func nanotime1() int64\n\t\t\t\t\t\"runtime.nanotime1\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tsetInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func walltime() (sec int64, nsec int32)\n\t\t\t\t\t\"runtime.walltime\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst msec = (new Date).getTime();\n\t\t\t\t\t\tsetInt64(sp + 8, msec / 1000);\n\t\t\t\t\t\tthis.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func scheduleTimeoutEvent(delay int64) int32\n\t\t\t\t\t\"runtime.scheduleTimeoutEvent\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst id = this._nextCallbackTimeoutID;\n\t\t\t\t\t\tthis._nextCallbackTimeoutID++;\n\t\t\t\t\t\tthis._scheduledTimeouts.set(id, setTimeout(\n\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\tthis._resume();\n\t\t\t\t\t\t\t\twhile (this._scheduledTimeouts.has(id)) {\n\t\t\t\t\t\t\t\t\t// for some reason Go failed to register the timeout event, log and try again\n\t\t\t\t\t\t\t\t\t// (temporary workaround for https://github.com/golang/go/issues/28975)\n\t\t\t\t\t\t\t\t\tconsole.warn(\"scheduleTimeoutEvent: missed timeout event\");\n\t\t\t\t\t\t\t\t\tthis._resume();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tgetInt64(sp + 8),\n\t\t\t\t\t\t));\n\t\t\t\t\t\tthis.mem.setInt32(sp + 16, id, true);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func clearTimeoutEvent(id int32)\n\t\t\t\t\t\"runtime.clearTimeoutEvent\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst id = this.mem.getInt32(sp + 8, true);\n\t\t\t\t\t\tclearTimeout(this._scheduledTimeouts.get(id));\n\t\t\t\t\t\tthis._scheduledTimeouts.delete(id);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func getRandomData(r []byte)\n\t\t\t\t\t\"runtime.getRandomData\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tcrypto.getRandomValues(loadSlice(sp + 8));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func finalizeRef(v ref)\n\t\t\t\t\t\"syscall/js.finalizeRef\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst id = this.mem.getUint32(sp + 8, true);\n\t\t\t\t\t\tthis._goRefCounts[id]--;\n\t\t\t\t\t\tif (this._goRefCounts[id] === 0) {\n\t\t\t\t\t\t\tconst v = this._values[id];\n\t\t\t\t\t\t\tthis._values[id] = null;\n\t\t\t\t\t\t\tthis._ids.delete(v);\n\t\t\t\t\t\t\tthis._idPool.push(id);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func stringVal(value string) ref\n\t\t\t\t\t\"syscall/js.stringVal\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tstoreValue(sp + 24, loadString(sp + 8));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueGet(v ref, p string) ref\n\t\t\t\t\t\"syscall/js.valueGet\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));\n\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\tstoreValue(sp + 32, result);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueSet(v ref, p string, x ref)\n\t\t\t\t\t\"syscall/js.valueSet\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tReflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueDelete(v ref, p string)\n\t\t\t\t\t\"syscall/js.valueDelete\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tReflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueIndex(v ref, i int) ref\n\t\t\t\t\t\"syscall/js.valueIndex\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tstoreValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));\n\t\t\t\t\t},\n\n\t\t\t\t\t// valueSetIndex(v ref, i int, x ref)\n\t\t\t\t\t\"syscall/js.valueSetIndex\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tReflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueCall(v ref, m string, args []ref) (ref, bool)\n\t\t\t\t\t\"syscall/js.valueCall\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst v = loadValue(sp + 8);\n\t\t\t\t\t\t\tconst m = Reflect.get(v, loadString(sp + 16));\n\t\t\t\t\t\t\tconst args = loadSliceOfValues(sp + 32);\n\t\t\t\t\t\t\tconst result = Reflect.apply(m, v, args);\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 56, result);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 64, 1);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 56, err);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 64, 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueInvoke(v ref, args []ref) (ref, bool)\n\t\t\t\t\t\"syscall/js.valueInvoke\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst v = loadValue(sp + 8);\n\t\t\t\t\t\t\tconst args = loadSliceOfValues(sp + 16);\n\t\t\t\t\t\t\tconst result = Reflect.apply(v, undefined, args);\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, result);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, err);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueNew(v ref, args []ref) (ref, bool)\n\t\t\t\t\t\"syscall/js.valueNew\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst v = loadValue(sp + 8);\n\t\t\t\t\t\t\tconst args = loadSliceOfValues(sp + 16);\n\t\t\t\t\t\t\tconst result = Reflect.construct(v, args);\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, result);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, err);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueLength(v ref) int\n\t\t\t\t\t\"syscall/js.valueLength\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tsetInt64(sp + 16, parseInt(loadValue(sp + 8).length));\n\t\t\t\t\t},\n\n\t\t\t\t\t// valuePrepareString(v ref) (ref, int)\n\t\t\t\t\t\"syscall/js.valuePrepareString\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst str = encoder.encode(String(loadValue(sp + 8)));\n\t\t\t\t\t\tstoreValue(sp + 16, str);\n\t\t\t\t\t\tsetInt64(sp + 24, str.length);\n\t\t\t\t\t},\n\n\t\t\t\t\t// valueLoadString(v ref, b []byte)\n\t\t\t\t\t\"syscall/js.valueLoadString\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst str = loadValue(sp + 8);\n\t\t\t\t\t\tloadSlice(sp + 16).set(str);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueInstanceOf(v ref, t ref) bool\n\t\t\t\t\t\"syscall/js.valueInstanceOf\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tthis.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func copyBytesToGo(dst []byte, src ref) (int, bool)\n\t\t\t\t\t\"syscall/js.copyBytesToGo\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst dst = loadSlice(sp + 8);\n\t\t\t\t\t\tconst src = loadValue(sp + 32);\n\t\t\t\t\t\tif (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst toCopy = src.subarray(0, dst.length);\n\t\t\t\t\t\tdst.set(toCopy);\n\t\t\t\t\t\tsetInt64(sp + 40, toCopy.length);\n\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func copyBytesToJS(dst ref, src []byte) (int, bool)\n\t\t\t\t\t\"syscall/js.copyBytesToJS\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst dst = loadValue(sp + 8);\n\t\t\t\t\t\tconst src = loadSlice(sp + 16);\n\t\t\t\t\t\tif (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst toCopy = src.subarray(0, dst.length);\n\t\t\t\t\t\tdst.set(toCopy);\n\t\t\t\t\t\tsetInt64(sp + 40, toCopy.length);\n\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t},\n\n\t\t\t\t\t\"debug\": (value) => {\n\t\t\t\t\t\tconsole.log(value);\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\n\t\tasync run(instance) {\n\t\t\tif (!(instance instanceof WebAssembly.Instance)) {\n\t\t\t\tthrow new Error(\"Go.run: WebAssembly.Instance expected\");\n\t\t\t}\n\t\t\tthis._inst = instance;\n\t\t\tthis.mem = new DataView(this._inst.exports.mem.buffer);\n\t\t\tthis._values = [ // JS values that Go currently has references to, indexed by reference id\n\t\t\t\tNaN,\n\t\t\t\t0,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\tglobalThis,\n\t\t\t\tthis,\n\t\t\t];\n\t\t\tthis._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id\n\t\t\tthis._ids = new Map([ // mapping from JS values to reference ids\n\t\t\t\t[0, 1],\n\t\t\t\t[null, 2],\n\t\t\t\t[true, 3],\n\t\t\t\t[false, 4],\n\t\t\t\t[globalThis, 5],\n\t\t\t\t[this, 6],\n\t\t\t]);\n\t\t\tthis._idPool = [];   // unused ids that have been garbage collected\n\t\t\tthis.exited = false; // whether the Go program has exited\n\n\t\t\t// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.\n\t\t\tlet offset = 4096;\n\n\t\t\tconst strPtr = (str) => {\n\t\t\t\tconst ptr = offset;\n\t\t\t\tconst bytes = encoder.encode(str + \"\\0\");\n\t\t\t\tnew Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);\n\t\t\t\toffset += bytes.length;\n\t\t\t\tif (offset % 8 !== 0) {\n\t\t\t\t\toffset += 8 - (offset % 8);\n\t\t\t\t}\n\t\t\t\treturn ptr;\n\t\t\t};\n\n\t\t\tconst argc = this.argv.length;\n\n\t\t\tconst argvPtrs = [];\n\t\t\tthis.argv.forEach((arg) => {\n\t\t\t\targvPtrs.push(strPtr(arg));\n\t\t\t});\n\t\t\targvPtrs.push(0);\n\n\t\t\tconst keys = Object.keys(this.env).sort();\n\t\t\tkeys.forEach((key) => {\n\t\t\t\targvPtrs.push(strPtr(`${key}=${this.env[key]}`));\n\t\t\t});\n\t\t\targvPtrs.push(0);\n\n\t\t\tconst argv = offset;\n\t\t\targvPtrs.forEach((ptr) => {\n\t\t\t\tthis.mem.setUint32(offset, ptr, true);\n\t\t\t\tthis.mem.setUint32(offset + 4, 0, true);\n\t\t\t\toffset += 8;\n\t\t\t});\n\n\t\t\t// The linker guarantees global data starts from at least wasmMinDataAddr.\n\t\t\t// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.\n\t\t\tconst wasmMinDataAddr = 4096 + 8192;\n\t\t\tif (offset >= wasmMinDataAddr) {\n\t\t\t\tthrow new Error(\"total length of command line and environment variables exceeds limit\");\n\t\t\t}\n\n\t\t\tthis._inst.exports.run(argc, argv);\n\t\t\tif (this.exited) {\n\t\t\t\tthis._resolveExitPromise();\n\t\t\t}\n\t\t\tawait this._exitPromise;\n\t\t}\n\n\t\t_resume() {\n\t\t\tif (this.exited) {\n\t\t\t\tthrow new Error(\"Go program has already exited\");\n\t\t\t}\n\t\t\tthis._inst.exports.resume();\n\t\t\tif (this.exited) {\n\t\t\t\tthis._resolveExitPromise();\n\t\t\t}\n\t\t}\n\n\t\t_makeFuncWrapper(id) {\n\t\t\tconst go = this;\n\t\t\treturn function () {\n\t\t\t\tconst event = { id: id, this: this, args: arguments };\n\t\t\t\tgo._pendingEvent = event;\n\t\t\t\tgo._resume();\n\t\t\t\treturn event.result;\n\t\t\t};\n\t\t}\n\t}\n})();\n"
 
-	appJS = "// -----------------------------------------------------------------------------\n// go-app\n// -----------------------------------------------------------------------------\nvar goappNav = function () {};\n\nvar goappUpdatedBeforeWasmLoaded = false;\nvar goappOnUpdate = function () {\n  goappUpdatedBeforeWasmLoaded = true;\n};\n\nvar goappAppInstallChangedBeforeWasmLoaded = false;\nvar goappOnAppInstallChange = function () {\n  goappAppInstallChangedBeforeWasmLoaded = true;\n};\n\nconst goappEnv = {{.Env}};\nconst goappLoadingLabel = \"{{.LoadingLabel}}\";\nconst goappWasmContentLength = \"{{.WasmContentLength}}\";\nconst goappWasmContentLengthHeader = \"{{.WasmContentLengthHeader}}\";\n\nlet goappServiceWorkerRegistration;\nlet deferredPrompt = null;\n\ngoappInitServiceWorker();\ngoappWatchForUpdate();\ngoappWatchForInstallable();\ngoappInitWebAssembly();\n\n// -----------------------------------------------------------------------------\n// Service Worker\n// -----------------------------------------------------------------------------\nasync function goappInitServiceWorker() {\n  if (\"serviceWorker\" in navigator) {\n    window.addEventListener(\"load\", async () => {\n      try {\n        const registration = await navigator.serviceWorker.register(\n          \"{{.WorkerJS}}\"\n        );\n        goappServiceWorkerRegistration = registration;\n        goappSetupNotifyUpdate(registration);\n        goappSetupPushNotification();\n      } catch (err) {\n        console.error(\"goapp service worker registration failed: \", err);\n      }\n    });\n  }\n}\n\n// -----------------------------------------------------------------------------\n// Update\n// -----------------------------------------------------------------------------\nfunction goappWatchForUpdate() {\n  window.addEventListener(\"beforeinstallprompt\", (e) => {\n    e.preventDefault();\n    deferredPrompt = e;\n    goappOnAppInstallChange();\n  });\n}\n\nfunction goappSetupNotifyUpdate(registration) {\n  registration.addEventListener(\"updatefound\", (event) => {\n    const newSW = registration.installing;\n    newSW.addEventListener(\"statechange\", (event) => {\n      if (!navigator.serviceWorker.controller) {\n        return;\n      }\n      if (newSW.state != \"activated\") {\n        return;\n      }\n      goappOnUpdate();\n    });\n  });\n}\n\nfunction goappTryUpdate() {\n  if (!goappServiceWorkerRegistration) {\n    return;\n  }\n  goappServiceWorkerRegistration.update();\n}\n\n// -----------------------------------------------------------------------------\n// Install\n// -----------------------------------------------------------------------------\nfunction goappWatchForInstallable() {\n  window.addEventListener(\"appinstalled\", () => {\n    deferredPrompt = null;\n    goappOnAppInstallChange();\n  });\n}\n\nfunction goappIsAppInstallable() {\n  return !goappIsAppInstalled() && deferredPrompt != null;\n}\n\nfunction goappIsAppInstalled() {\n  const isStandalone = window.matchMedia(\"(display-mode: standalone)\").matches;\n  return isStandalone || navigator.standalone;\n}\n\nasync function goappShowInstallPrompt() {\n  deferredPrompt.prompt();\n  await deferredPrompt.userChoice;\n  deferredPrompt = null;\n}\n\n// -----------------------------------------------------------------------------\n// Environment\n// -----------------------------------------------------------------------------\nfunction goappGetenv(k) {\n  return goappEnv[k];\n}\n\n// -----------------------------------------------------------------------------\n// Notifications\n// -----------------------------------------------------------------------------\nfunction goappSetupPushNotification() {\n  navigator.serviceWorker.addEventListener(\"message\", (event) => {\n    const msg = event.data.goapp;\n    if (!msg) {\n      return;\n    }\n\n    if (msg.type !== \"notification\") {\n      return;\n    }\n\n    goappNav(msg.path);\n  });\n}\n\nasync function goappSubscribePushNotifications(vapIDpublicKey) {\n  try {\n    const subscription =\n      await goappServiceWorkerRegistration.pushManager.subscribe({\n        userVisibleOnly: true,\n        applicationServerKey: vapIDpublicKey,\n      });\n    return JSON.stringify(subscription);\n  } catch (err) {\n    console.error(err);\n    return \"\";\n  }\n}\n\nfunction goappNewNotification(jsonNotification) {\n  let notification = JSON.parse(jsonNotification);\n\n  const title = notification.title;\n  delete notification.title;\n\n  let path = notification.path;\n  if (!path) {\n    path = \"/\";\n  }\n\n  const webNotification = new Notification(title, notification);\n\n  webNotification.onclick = () => {\n    goappNav(path);\n    webNotification.close();\n  };\n}\n\n// -----------------------------------------------------------------------------\n// Keep Clean Body\n// -----------------------------------------------------------------------------\nfunction goappKeepBodyClean() {\n  const body = document.body;\n  const bodyChildrenCount = body.children.length;\n\n  const mutationObserver = new MutationObserver(function (mutationList) {\n    mutationList.forEach((mutation) => {\n      switch (mutation.type) {\n        case \"childList\":\n          while (body.children.length > bodyChildrenCount) {\n            body.removeChild(body.lastChild);\n          }\n          break;\n      }\n    });\n  });\n\n  mutationObserver.observe(document.body, {\n    childList: true,\n  });\n\n  return () => mutationObserver.disconnect();\n}\n\n// -----------------------------------------------------------------------------\n// Web Assembly\n// -----------------------------------------------------------------------------\nasync function goappInitWebAssembly() {\n  const loader = document.getElementById(\"app-wasm-loader\");\n\n  if (!goappCanLoadWebAssembly()) {\n    loader.remove();\n    return;\n  }\n\n  let instantiateStreaming = WebAssembly.instantiateStreaming;\n  if (!instantiateStreaming) {\n    instantiateStreaming = async (resp, importObject) => {\n      const source = await (await resp).arrayBuffer();\n      return await WebAssembly.instantiate(source, importObject);\n    };\n  }\n\n  const loaderIcon = document.getElementById(\"app-wasm-loader-icon\");\n  const loaderLabel = document.getElementById(\"app-wasm-loader-label\");\n\n  try {\n    const showProgress = (progress) => {\n      loaderLabel.innerText = goappLoadingLabel.replace(\"{progress}\", progress);\n    };\n    showProgress(0);\n\n    const go = new Go();\n    const wasm = await instantiateStreaming(\n      fetchWithProgress(\"{{.Wasm}}\", showProgress),\n      go.importObject\n    );\n\n    go.run(wasm.instance);\n    loader.remove();\n  } catch (err) {\n    loaderIcon.className = \"goapp-logo\";\n    loaderLabel.innerText = err;\n    console.error(\"loading wasm failed: \", err);\n  }\n}\n\nfunction goappCanLoadWebAssembly() {\n  if (\n    /bot|googlebot|crawler|spider|robot|crawling/i.test(navigator.userAgent)\n  ) {\n    return false;\n  }\n\n  const urlParams = new URLSearchParams(window.location.search);\n  return urlParams.get(\"wasm\") !== \"false\";\n}\n\nasync function fetchWithProgress(url, progess) {\n  const response = await fetch(url);\n\n  let contentLength = goappWasmContentLength;\n  if (contentLength <= 0) {\n    try {\n      contentLength = response.headers.get(goappWasmContentLengthHeader);\n    } catch {}\n    if (!goappWasmContentLengthHeader || !contentLength) {\n      contentLength = response.headers.get(\"Content-Length\");\n    }\n  }\n\n  const total = parseInt(contentLength, 10);\n  let loaded = 0;\n\n  const progressHandler = function (loaded, total) {\n    progess(Math.round((loaded * 100) / total));\n  };\n\n  var res = new Response(\n    new ReadableStream(\n      {\n        async start(controller) {\n          var reader = response.body.getReader();\n          for (;;) {\n            var { done, value } = await reader.read();\n\n            if (done) {\n              progressHandler(total, total);\n              break;\n            }\n\n            loaded += value.byteLength;\n            progressHandler(loaded, total);\n            controller.enqueue(value);\n          }\n          controller.close();\n        },\n      },\n      {\n        status: response.status,\n        statusText: response.statusText,\n      }\n    )\n  );\n\n  for (var pair of response.headers.entries()) {\n    res.headers.set(pair[0], pair[1]);\n  }\n\n  return res;\n}\n"
+	appJS = "// -----------------------------------------------------------------------------\n// go-app\n// -----------------------------------------------------------------------------\nvar goappNav = function () {};\n\nvar goappUpdatedBeforeWasmLoaded = false;\nvar goappOnUpdate = function () {\n  goappUpdatedBeforeWasmLoaded = true;\n};\n\nvar goappAppInstallChangedBeforeWasmLoaded = false;\nvar goappOnAppInstallChange = function () {\n  goappAppInstallChangedBeforeWasmLoaded = true;\n};\n\nconst goappEnv = {{.Env}};\nconst goappLoadingLabel = \"{{.LoadingLabel}}\";\nconst goappWasmContentLength = \"{{.WasmContentLength}}\";\nconst goappWasmContentLengthHeader = \"{{.WasmContentLengthHeader}}\";\n\nlet goappServiceWorkerRegistration;\nlet deferredPrompt = null;\n\ngoappInitServiceWorker();\ngoappWatchForUpdate();\ngoappWatchForInstallable();\ngoappInitWebAssembly();\n\n// -----------------------------------------------------------------------------\n// Service Worker\n// -----------------------------------------------------------------------------\nasync function goappInitServiceWorker() {\n  if (\"serviceWorker\" in navigator) {\n    try {\n      const registration = await navigator.serviceWorker.register(\n        \"{{.WorkerJS}}\"\n      );\n      goappServiceWorkerRegistration = registration;\n      goappSetupNotifyUpdate(registration);\n      goappSetupPushNotification();\n    } catch (err) {\n      console.error(\"goapp service worker registration failed: \", err);\n    }\n  }\n}\n\n// -----------------------------------------------------------------------------\n// Update\n// -----------------------------------------------------------------------------\nfunction goappWatchForUpdate() {\n  window.addEventListener(\"beforeinstallprompt\", (e) => {\n    e.preventDefault();\n    deferredPrompt = e;\n    goappOnAppInstallChange();\n  });\n}\n\nfunction goappSetupNotifyUpdate(registration) {\n  registration.addEventListener(\"updatefound\", (event) => {\n    const newSW = registration.installing;\n    newSW.addEventListener(\"statechange\", (event) => {\n      if (!navigator.serviceWorker.controller) {\n        return;\n      }\n\n      switch (newSW.state) {\n        case \"installed\":\n          goappOnUpdate();\n      }\n    });\n  });\n}\n\nfunction goappTryUpdate() {\n  if (!goappServiceWorkerRegistration) {\n    return;\n  }\n  goappServiceWorkerRegistration.update();\n}\n\n// -----------------------------------------------------------------------------\n// Install\n// -----------------------------------------------------------------------------\nfunction goappWatchForInstallable() {\n  window.addEventListener(\"appinstalled\", () => {\n    deferredPrompt = null;\n    goappOnAppInstallChange();\n  });\n}\n\nfunction goappIsAppInstallable() {\n  return !goappIsAppInstalled() && deferredPrompt != null;\n}\n\nfunction goappIsAppInstalled() {\n  const isStandalone = window.matchMedia(\"(display-mode: standalone)\").matches;\n  return isStandalone || navigator.standalone;\n}\n\nasync function goappShowInstallPrompt() {\n  deferredPrompt.prompt();\n  await deferredPrompt.userChoice;\n  deferredPrompt = null;\n}\n\n// -----------------------------------------------------------------------------\n// Environment\n// -----------------------------------------------------------------------------\nfunction goappGetenv(k) {\n  return goappEnv[k];\n}\n\n// -----------------------------------------------------------------------------\n// Notifications\n// -----------------------------------------------------------------------------\nfunction goappSetupPushNotification() {\n  navigator.serviceWorker.addEventListener(\"message\", (event) => {\n    const msg = event.data.goapp;\n    if (!msg) {\n      return;\n    }\n\n    if (msg.type !== \"notification\") {\n      return;\n    }\n\n    goappNav(msg.path);\n  });\n}\n\nasync function goappSubscribePushNotifications(vapIDpublicKey) {\n  try {\n    const subscription =\n      await goappServiceWorkerRegistration.pushManager.subscribe({\n        userVisibleOnly: true,\n        applicationServerKey: vapIDpublicKey,\n      });\n    return JSON.stringify(subscription);\n  } catch (err) {\n    console.error(err);\n    return \"\";\n  }\n}\n\nfunction goappNewNotification(jsonNotification) {\n  let notification = JSON.parse(jsonNotification);\n\n  const title = notification.title;\n  delete notification.title;\n\n  let path = notification.path;\n  if (!path) {\n    path = \"/\";\n  }\n\n  const webNotification = new Notification(title, notification);\n\n  webNotification.onclick = () => {\n    goappNav(path);\n    webNotification.close();\n  };\n}\n\n// -----------------------------------------------------------------------------\n// Keep Clean Body\n// -----------------------------------------------------------------------------\nfunction goappKeepBodyClean() {\n  const body = document.body;\n  const bodyChildrenCount = body.children.length;\n\n  const mutationObserver = new MutationObserver(function (mutationList) {\n    mutationList.forEach((mutation) => {\n      switch (mutation.type) {\n        case \"childList\":\n          while (body.children.length > bodyChildrenCount) {\n            body.removeChild(body.lastChild);\n          }\n          break;\n      }\n    });\n  });\n\n  mutationObserver.observe(document.body, {\n    childList: true,\n  });\n\n  return () => mutationObserver.disconnect();\n}\n\n// -----------------------------------------------------------------------------\n// Web Assembly\n// -----------------------------------------------------------------------------\nasync function goappInitWebAssembly() {\n  const loader = document.getElementById(\"app-wasm-loader\");\n\n  if (!goappCanLoadWebAssembly()) {\n    loader.remove();\n    return;\n  }\n\n  let instantiateStreaming = WebAssembly.instantiateStreaming;\n  if (!instantiateStreaming) {\n    instantiateStreaming = async (resp, importObject) => {\n      const source = await (await resp).arrayBuffer();\n      return await WebAssembly.instantiate(source, importObject);\n    };\n  }\n\n  const loaderIcon = document.getElementById(\"app-wasm-loader-icon\");\n  const loaderLabel = document.getElementById(\"app-wasm-loader-label\");\n\n  try {\n    const showProgress = (progress) => {\n      loaderLabel.innerText = goappLoadingLabel.replace(\"{progress}\", progress);\n    };\n    showProgress(0);\n\n    const go = new Go();\n    const wasm = await instantiateStreaming(\n      fetchWithProgress(\"{{.Wasm}}\", showProgress),\n      go.importObject\n    );\n\n    go.run(wasm.instance);\n    loader.remove();\n  } catch (err) {\n    loaderIcon.className = \"goapp-logo\";\n    loaderLabel.innerText = err;\n    console.error(\"loading wasm failed: \", err);\n  }\n}\n\nfunction goappCanLoadWebAssembly() {\n  if (\n    /bot|googlebot|crawler|spider|robot|crawling/i.test(navigator.userAgent)\n  ) {\n    return false;\n  }\n\n  const urlParams = new URLSearchParams(window.location.search);\n  return urlParams.get(\"wasm\") !== \"false\";\n}\n\nasync function fetchWithProgress(url, progess) {\n  const response = await fetch(url);\n\n  let contentLength = goappWasmContentLength;\n  if (contentLength <= 0) {\n    try {\n      contentLength = response.headers.get(goappWasmContentLengthHeader);\n    } catch {}\n    if (!goappWasmContentLengthHeader || !contentLength) {\n      contentLength = response.headers.get(\"Content-Length\");\n    }\n  }\n\n  const total = parseInt(contentLength, 10);\n  let loaded = 0;\n\n  const progressHandler = function (loaded, total) {\n    progess(Math.round((loaded * 100) / total));\n  };\n\n  var res = new Response(\n    new ReadableStream(\n      {\n        async start(controller) {\n          var reader = response.body.getReader();\n          for (;;) {\n            var { done, value } = await reader.read();\n\n            if (done) {\n              progressHandler(total, total);\n              break;\n            }\n\n            loaded += value.byteLength;\n            progressHandler(loaded, total);\n            controller.enqueue(value);\n          }\n          controller.close();\n        },\n      },\n      {\n        status: response.status,\n        statusText: response.statusText,\n      }\n    )\n  );\n\n  for (var pair of response.headers.entries()) {\n    res.headers.set(pair[0], pair[1]);\n  }\n\n  return res;\n}\n"
 
 	manifestJSON = "{\n  \"short_name\": \"{{.ShortName}}\",\n  \"name\": \"{{.Name}}\",\n  \"description\": \"{{.Description}}\",\n  \"icons\": [\n    {\n      \"src\": \"{{.SVGIcon}}\",\n      \"type\": \"image/svg+xml\",\n      \"sizes\": \"any\"\n    },\n    {\n      \"src\": \"{{.LargeIcon}}\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    },\n    {\n      \"src\": \"{{.DefaultIcon}}\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"{{.MaskableIcon}}\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\",\n      \"sizes\": \"192x192\"\n    }\n  ],\n  \"scope\": \"{{.Scope}}\",\n  \"start_url\": \"{{.StartURL}}\",\n  \"background_color\": \"{{.BackgroundColor}}\",\n  \"theme_color\": \"{{.ThemeColor}}\",\n  \"display\": \"standalone\"\n}"
 


Test patch:
diff --git a/pkg/app/engine_test.go b/pkg/app/engine_test.go
index 61fe20de..28f6f7a6 100644
--- a/pkg/app/engine_test.go
+++ b/pkg/app/engine_test.go
@@ -266,7 +266,7 @@ func TestEngineEncode(t *testing.T) {
 			),
 		))
 		require.NoError(t, err)
-		require.Equal(t, "<!DOCTYPE html>\n<html>\n  <body>\n    <span></span>\n    bye\n  </body>\n</html>", b.String())
+		require.Equal(t, "<!doctype html>\n<html>\n  <body>\n    <span></span>\n    bye\n  </body>\n</html>", b.String())
 	})
 }
 
diff --git a/pkg/app/http_test.go b/pkg/app/http_test.go
index 9751a8e0..796e76bc 100644
--- a/pkg/app/http_test.go
+++ b/pkg/app/http_test.go
@@ -263,8 +263,8 @@ func TestHandlerServeAppWorkerJSWithLocalDir(t *testing.T) {
 	body := w.Body.String()
 	require.Equal(t, http.StatusOK, w.Code)
 	require.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
-	require.Contains(t, body, `self.addEventListener("install", (event) => {`)
-	require.Contains(t, body, `self.addEventListener("activate", (event) => {`)
+	require.Contains(t, body, `self.addEventListener("install", async (event) => {`)
+	require.Contains(t, body, `self.addEventListener("activate", async (event) => {`)
 	require.Contains(t, body, `self.addEventListener("fetch", (event) => {`)
 	require.Contains(t, body, `"/web/hello.css"`)
 	require.Contains(t, body, `"/web/hello.js"`)
@@ -294,8 +294,8 @@ func TestHandlerServeAppWorkerJSWithRemoteBucket(t *testing.T) {
 	body := w.Body.String()
 	require.Equal(t, http.StatusOK, w.Code)
 	require.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
-	require.Contains(t, body, `self.addEventListener("install", (event) => {`)
-	require.Contains(t, body, `self.addEventListener("activate", (event) => {`)
+	require.Contains(t, body, `self.addEventListener("install", async (event) => {`)
+	require.Contains(t, body, `self.addEventListener("activate", async (event) => {`)
 	require.Contains(t, body, `self.addEventListener("fetch", (event) => {`)
 	require.Contains(t, body, `"https://storage.googleapis.com/go-app/web/hello.css"`)
 	require.Contains(t, body, `"https://storage.googleapis.com/go-app/web/hello.js"`)
@@ -325,8 +325,8 @@ func TestHandlerServeAppWorkerJSWithGitHubPages(t *testing.T) {
 	body := w.Body.String()
 	require.Equal(t, http.StatusOK, w.Code)
 	require.Equal(t, "application/javascript", w.Header().Get("Content-Type"))
-	require.Contains(t, body, `self.addEventListener("install", (event) => {`)
-	require.Contains(t, body, `self.addEventListener("activate", (event) => {`)
+	require.Contains(t, body, `self.addEventListener("install", async (event) => {`)
+	require.Contains(t, body, `self.addEventListener("activate", async (event) => {`)
 	require.Contains(t, body, `self.addEventListener("fetch", (event) => {`)
 	require.Contains(t, body, `"/go-app/web/hello.css"`)
 	require.Contains(t, body, `"/go-app/web/hello.js"`)