summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2024-02-21 01:08:25 -0500
committerAAGaming <aagaming@riseup.net>2024-02-21 01:08:25 -0500
commit6d2e9365c0fb1bea804743245d79a5b97e3af108 (patch)
treed34d09b7406d065edc79ff4a6c694f5caee923f0
parent61cf80f8a2d472e9cbc7d401c7cd24075d5bcf28 (diff)
downloaddecky-loader-6d2e9365c0fb1bea804743245d79a5b97e3af108.tar.gz
decky-loader-6d2e9365c0fb1bea804743245d79a5b97e3af108.zip
more major websocket progress
-rw-r--r--backend/decky_loader/browser.py18
-rw-r--r--backend/decky_loader/customtypes.py6
-rw-r--r--backend/decky_loader/enums.py10
-rw-r--r--backend/decky_loader/helpers.py16
-rw-r--r--backend/decky_loader/loader.py18
-rw-r--r--backend/decky_loader/localplatform/localplatformlinux.py3
-rw-r--r--backend/decky_loader/localplatform/localplatformwin.py2
-rw-r--r--backend/decky_loader/main.py2
-rw-r--r--backend/decky_loader/plugin/plugin.py6
-rw-r--r--backend/decky_loader/plugin/sandboxed_plugin.py2
-rw-r--r--backend/decky_loader/settings.py2
-rw-r--r--backend/decky_loader/updater.py10
-rw-r--r--backend/decky_loader/wsrouter.py2
-rw-r--r--backend/locales/en-US.json10
-rw-r--r--frontend/pnpm-lock.yaml6
-rw-r--r--frontend/src/components/modals/filepicker/patches/library.ts8
-rw-r--r--frontend/src/components/settings/pages/developer/index.tsx6
-rw-r--r--frontend/src/components/settings/pages/general/Updater.tsx4
-rw-r--r--frontend/src/components/settings/pages/testing/index.tsx96
-rw-r--r--frontend/src/components/store/Store.tsx4
-rw-r--r--frontend/src/plugin-loader.tsx321
-rw-r--r--frontend/src/plugin.ts5
-rw-r--r--frontend/src/start.tsx5
-rw-r--r--frontend/src/steamfixes/index.ts8
-rw-r--r--frontend/src/updater.ts2
-rw-r--r--frontend/src/wsrouter.ts24
26 files changed, 357 insertions, 239 deletions
diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py
index def81011..cb573b13 100644
--- a/backend/decky_loader/browser.py
+++ b/backend/decky_loader/browser.py
@@ -4,7 +4,7 @@ import json
# from pprint import pformat
# Partial imports
-from aiohttp import ClientSession
+from aiohttp import ClientSession, request
from asyncio import sleep
from hashlib import sha256
from io import BytesIO
@@ -123,7 +123,6 @@ class PluginBrowser:
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
self.loader.watcher.disabled = True
- tab = await get_gamepadui_tab()
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
@@ -131,8 +130,7 @@ class PluginBrowser:
logger.info("uninstalling " + name)
logger.info(" at dir " + plugin_dir)
logger.debug("calling frontend unload for %s" % str(name))
- res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
- logger.debug("result of unload from UI: %s", res)
+ await self.loader.ws.emit("loader/unload_plugin", name)
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
@@ -258,20 +256,14 @@ class PluginBrowser:
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
- tab = await get_gamepadui_tab()
- await tab.open_websocket()
- await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
+
+ await self.loader.ws.emit("loader/add_plugin_install_prompt", name, version, request_id, hash, install_type)
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
request_id = str(time())
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
- js_requests_parameter = ','.join([
- f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
- ])
- tab = await get_gamepadui_tab()
- await tab.open_websocket()
- await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
+ await self.loader.ws.emit("loader/add_multiple_plugins_install_prompt", request_id, requests)
async def confirm_plugin_install(self, request_id: str):
requestOrRequests = self.install_requests.pop(request_id)
diff --git a/backend/decky_loader/customtypes.py b/backend/decky_loader/customtypes.py
deleted file mode 100644
index 84ebc235..00000000
--- a/backend/decky_loader/customtypes.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from enum import Enum
-
-class UserType(Enum):
- HOST_USER = 1
- EFFECTIVE_USER = 2
- ROOT = 3 \ No newline at end of file
diff --git a/backend/decky_loader/enums.py b/backend/decky_loader/enums.py
new file mode 100644
index 00000000..e7fb4905
--- /dev/null
+++ b/backend/decky_loader/enums.py
@@ -0,0 +1,10 @@
+from enum import IntEnum
+
+class UserType(IntEnum):
+ HOST_USER = 1
+ EFFECTIVE_USER = 2
+ ROOT = 3
+
+class PluginLoadType(IntEnum):
+ LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
+ ESMODULE_V1 = 1 # esmodule loading with modern @decky/backend apis \ No newline at end of file
diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py
index 2d5eb6dd..f4005cc5 100644
--- a/backend/decky_loader/helpers.py
+++ b/backend/decky_loader/helpers.py
@@ -12,7 +12,7 @@ from aiohttp.web import Request, Response, middleware
from aiohttp.typedefs import Handler
from aiohttp import ClientSession
from .localplatform import localplatform
-from .customtypes import UserType
+from .enums import UserType
from logging import getLogger
from packaging.version import Version
@@ -23,6 +23,7 @@ csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
assets_regex = re.compile("^/plugins/.*/assets/.*")
+dist_regex = re.compile("^/plugins/.*/dist/.*")
frontend_regex = re.compile("^/frontend/.*")
logger = getLogger("Main")
@@ -34,7 +35,18 @@ def get_csrf_token():
@middleware
async def csrf_middleware(request: Request, handler: Handler):
- if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or str(request.rel_url.path) == "/ws" or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
+ if str(request.method) == "OPTIONS" or \
+ request.headers.get('Authentication') == csrf_token or \
+ str(request.rel_url) == "/auth/token" or \
+ str(request.rel_url).startswith("/plugins/load_main/") or \
+ str(request.rel_url).startswith("/static/") or \
+ str(request.rel_url).startswith("/steam_resource/") or \
+ str(request.rel_url).startswith("/frontend/") or \
+ str(request.rel_url.path) == "/ws" or \
+ assets_regex.match(str(request.rel_url)) or \
+ dist_regex.match(str(request.rel_url)) or \
+ frontend_regex.match(str(request.rel_url)):
+
return await handler(request)
return Response(text='Forbidden', status=403)
diff --git a/backend/decky_loader/loader.py b/backend/decky_loader/loader.py
index 550638a3..aad595e7 100644
--- a/backend/decky_loader/loader.py
+++ b/backend/decky_loader/loader.py
@@ -16,9 +16,9 @@ from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from .main import PluginManager
-from .injector import get_gamepadui_tab
from .plugin.plugin import PluginWrapper
from .wsrouter import WSRouter
+from .enums import PluginLoadType
Plugins = dict[str, PluginWrapper]
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
@@ -96,6 +96,7 @@ class Loader:
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/locales/{path:.*}", self.handle_frontend_locales),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
+ web.get("/plugins/{plugin_name}/dist/{path:.*}", self.handle_plugin_dist),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
])
@@ -126,7 +127,13 @@ class Loader:
async def get_plugins(self):
plugins = list(self.plugins.values())
- return [{"name": str(i), "version": i.version} for i in plugins]
+ return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins]
+
+ async def handle_plugin_dist(self, request: web.Request):
+ plugin = self.plugins[request.match_info["plugin_name"]]
+ file = path.join(self.plugin_path, plugin.plugin_directory, "dist", request.match_info["path"])
+
+ return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def handle_plugin_frontend_assets(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]]
@@ -145,7 +152,7 @@ class Loader:
async def plugin_emitted_event(event: str, data: Any):
self.logger.debug(f"PLUGIN EMITTED EVENT: {str(event)} {data}")
event_data = PluginEvent(plugin_name=plugin.name, event=event, data=data)
- await self.ws.emit("plugin_event", event_data)
+ await self.ws.emit("loader/plugin_event", event_data)
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
if plugin.name in self.plugins:
@@ -166,9 +173,8 @@ class Loader:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
- async def dispatch_plugin(self, name: str, version: str | None):
- gpui_tab = await get_gamepadui_tab()
- await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
+ async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
+ await self.ws.emit("loader/import_plugin", name, version, load_type)
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py
index 4eb112ee..2674e9bc 100644
--- a/backend/decky_loader/localplatform/localplatformlinux.py
+++ b/backend/decky_loader/localplatform/localplatformlinux.py
@@ -1,6 +1,6 @@
import os, pwd, grp, sys, logging
from subprocess import call, run, DEVNULL, PIPE, STDOUT
-from ..customtypes import UserType
+from ..enums import UserType
logger = logging.getLogger("localplatform")
@@ -157,6 +157,7 @@ async def service_start(service_name : str) -> bool:
return res.returncode == 0
async def restart_webhelper() -> bool:
+ logger.info("Restarting steamwebhelper")
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
return res.returncode == 0
diff --git a/backend/decky_loader/localplatform/localplatformwin.py b/backend/decky_loader/localplatform/localplatformwin.py
index f1a5be17..38e4b2b0 100644
--- a/backend/decky_loader/localplatform/localplatformwin.py
+++ b/backend/decky_loader/localplatform/localplatformwin.py
@@ -1,4 +1,4 @@
-from ..customtypes import UserType
+from ..enums import UserType
import os, sys
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
diff --git a/backend/decky_loader/main.py b/backend/decky_loader/main.py
index 9095f711..64c76dc0 100644
--- a/backend/decky_loader/main.py
+++ b/backend/decky_loader/main.py
@@ -30,7 +30,7 @@ from .loader import Loader
from .settings import SettingsManager
from .updater import Updater
from .utilities import Utilities
-from .customtypes import UserType
+from .enums import UserType
from .wsrouter import WSRouter
diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py
index 47d3d7b0..cad323f4 100644
--- a/backend/decky_loader/plugin/plugin.py
+++ b/backend/decky_loader/plugin/plugin.py
@@ -4,9 +4,9 @@ from logging import getLogger
from os import path
from multiprocessing import Process
-
from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType
+from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket
from typing import Any, Callable, Coroutine, Dict, List
@@ -21,10 +21,14 @@ class PluginWrapper:
self.version = None
+ self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
+
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"]
+ if ("type" in package_json and package_json["type"] == "module"):
+ self.load_type = PluginLoadType.ESMODULE_V1.value
self.name = json["name"]
self.author = json["author"]
diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py
index 3fd38e4f..b49dcf41 100644
--- a/backend/decky_loader/plugin/sandboxed_plugin.py
+++ b/backend/decky_loader/plugin/sandboxed_plugin.py
@@ -11,7 +11,7 @@ from asyncio import (get_event_loop, new_event_loop,
from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
-from ..customtypes import UserType
+from ..enums import UserType
from .. import helpers
from typing import List, TypeVar, Type
diff --git a/backend/decky_loader/settings.py b/backend/decky_loader/settings.py
index c0f2b90c..b5f034aa 100644
--- a/backend/decky_loader/settings.py
+++ b/backend/decky_loader/settings.py
@@ -2,7 +2,7 @@ from json import dump, load
from os import mkdir, path, listdir, rename
from typing import Any, Dict
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
-from .customtypes import UserType
+from .enums import UserType
from .helpers import get_homebrew_path
diff --git a/backend/decky_loader/updater.py b/backend/decky_loader/updater.py
index a28f0c11..6355fcc7 100644
--- a/backend/decky_loader/updater.py
+++ b/backend/decky_loader/updater.py
@@ -35,7 +35,6 @@ class TestingVersion(TypedDict):
link: str
head_sha: str
-
class Updater:
def __init__(self, context: PluginManager) -> None:
self.context = context
@@ -103,7 +102,7 @@ class Updater:
logger.debug("checking for updates")
selectedBranch = self.get_branch(self.context.settings)
async with ClientSession() as web:
- async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
+ async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
remoteVersions: List[RemoteVer] = await res.json()
if selectedBranch == 0:
logger.debug("release type: release")
@@ -126,8 +125,7 @@ class Updater:
logger.error("release type: NOT FOUND")
raise ValueError("no valid branch found")
logger.info("Updated remote version information")
- tab = await get_gamepadui_tab()
- await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
+ await self.context.ws.emit("loader/notify_updates")
return await self.get_version_info()
async def version_reloader(self):
@@ -158,7 +156,7 @@ class Updater:
raw += len(c)
new_progress = round((raw / total) * 100)
if progress != new_progress:
- self.context.loop.create_task(self.context.ws.emit("frontend/update_download_percentage", new_progress))
+ self.context.loop.create_task(self.context.ws.emit("updater/update_download_percentage", new_progress))
progress = new_progress
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
@@ -182,7 +180,7 @@ class Updater:
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
logger.info("Updated loader installation.")
- await self.context.ws.emit("frontend/finish_download")
+ await self.context.ws.emit("updater/finish_download")
await self.do_restart()
await tab.close_websocket()
diff --git a/backend/decky_loader/wsrouter.py b/backend/decky_loader/wsrouter.py
index 4874e967..918b74bc 100644
--- a/backend/decky_loader/wsrouter.py
+++ b/backend/decky_loader/wsrouter.py
@@ -93,9 +93,7 @@ class WSRouter:
async for msg in ws:
msg = cast(WSMessageExtra, msg)
- self.logger.debug(msg)
if msg.type == WSMsgType.TEXT:
- self.logger.debug(msg.data)
if msg.data == 'close':
# TODO DO NOT RELY ON THIS!
break
diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json
index ca18f7da..fe544dea 100644
--- a/backend/locales/en-US.json
+++ b/backend/locales/en-US.json
@@ -263,8 +263,10 @@
"reloading": "Reloading",
"updating": "Updating"
}
- },
- "Testing": {
- "download": "Download"
- }
+ },
+ "Testing": {
+ "download": "Download",
+ "header": "The following versions of Decky Loader are built from open third-party Pull Requests. The Decky Loader team has not verified their functionality or security, and they may be outdated.",
+ "loading": "Loading open Pull Requests..."
+ }
}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 705f935b..ef1a7d6b 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -7,7 +7,7 @@ settings:
dependencies:
decky-frontend-lib:
specifier: 3.24.5
- version: link:../../lib
+ version: 3.24.5
filesize:
specifier: ^10.0.7
version: 10.0.7
@@ -1482,6 +1482,10 @@ packages:
dependencies:
ms: 2.1.2
+ /decky-frontend-lib@3.24.5:
+ resolution: {integrity: sha512-eYlbKDOOcIBPI0b76Rqvlryq2ym/QNiry4xf2pFrXmBa1f95dflqbQAb2gTq9uHEa5gFmeV4lUcMPGJ3M14Xqw==}
+ dev: false
+
/decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
dependencies:
diff --git a/frontend/src/components/modals/filepicker/patches/library.ts b/frontend/src/components/modals/filepicker/patches/library.ts
index 076e78f6..71eb9541 100644
--- a/frontend/src/components/modals/filepicker/patches/library.ts
+++ b/frontend/src/components/modals/filepicker/patches/library.ts
@@ -1,6 +1,7 @@
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
import Logger from '../../../../logger';
+import { FileSelectionType } from '..';
const logger = new Logger('LibraryPatch');
@@ -13,7 +14,12 @@ function rePatch() {
const details = window.appDetailsStore.GetAppDetails(appid);
logger.debug('game details', details);
// strShortcutStartDir
- const file = await DeckyPluginLoader.openFilePicker(details?.strShortcutStartDir.replaceAll('"', '') || '/');
+ const file = await DeckyPluginLoader.openFilePicker(
+ FileSelectionType.FILE,
+ details?.strShortcutStartDir.replaceAll('"', '') || '/',
+ true,
+ true,
+ );
logger.debug('user selected', file);
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
const pathArr = file.path.split('/');
diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx
index 9c8504e7..091e367e 100644
--- a/frontend/src/components/settings/pages/developer/index.tsx
+++ b/frontend/src/components/settings/pages/developer/index.tsx
@@ -28,7 +28,7 @@ const installFromZip = async () => {
logger.error('The default path has not been found!');
return;
}
- DeckyPluginLoader.openFilePickerV2(FileSelectionType.FILE, path, true, true, undefined, ['zip'], false, false).then(
+ DeckyPluginLoader.openFilePicker(FileSelectionType.FILE, path, true, true, undefined, ['zip'], false, false).then(
(val) => {
const url = `file://${val.path}`;
console.log(`Installing plugin locally from ${url}`);
@@ -37,6 +37,8 @@ const installFromZip = async () => {
);
};
+const getTabID = DeckyBackend.callable<[name: string], string>('utilities/get_tab_id');
+
export default function DeveloperSettings() {
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
@@ -85,7 +87,7 @@ export default function DeveloperSettings() {
<DialogButton
onClick={async () => {
try {
- let tabId = await DeckyBackend.call<[name: string], string>('utilities/get_tab_id', 'SharedJSContext');
+ let tabId = await getTabID('SharedJSContext');
Navigation.NavigateToExternalWeb(
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + tabId,
);
diff --git a/frontend/src/components/settings/pages/general/Updater.tsx b/frontend/src/components/settings/pages/general/Updater.tsx
index c563bbca..3c7e53f1 100644
--- a/frontend/src/components/settings/pages/general/Updater.tsx
+++ b/frontend/src/components/settings/pages/general/Updater.tsx
@@ -75,12 +75,12 @@ export default function UpdaterSettings() {
const { t } = useTranslation();
useEffect(() => {
- const a = DeckyBackend.addEventListener('frontend/update_download_percentage', (percentage) => {
+ const a = DeckyBackend.addEventListener('updater/update_download_percentage', (percentage) => {
setUpdateProgress(percentage);
setIsLoaderUpdating(true);
});
- const b = DeckyBackend.addEventListener('frontend/finish_download', () => {
+ const b = DeckyBackend.addEventListener('updater/finish_download', () => {
setUpdateProgress(0);
setReloading(true);
});
diff --git a/frontend/src/components/settings/pages/testing/index.tsx b/frontend/src/components/settings/pages/testing/index.tsx
index 72267295..cdf51c71 100644
--- a/frontend/src/components/settings/pages/testing/index.tsx
+++ b/frontend/src/components/settings/pages/testing/index.tsx
@@ -1,4 +1,12 @@
-import { DialogBody, DialogButton, DialogControlsSection, Focusable, Navigation } from 'decky-frontend-lib';
+import {
+ DialogBody,
+ DialogButton,
+ DialogControlsSection,
+ Field,
+ Focusable,
+ Navigation,
+ SteamSpinner,
+} from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaInfo } from 'react-icons/fa';
@@ -19,13 +27,23 @@ const downloadTestingVersion = DeckyBackend.callable<[pr_id: number, sha: string
export default function TestingVersionList() {
const { t } = useTranslation();
const [testingVersions, setTestingVersions] = useState<TestingVersion[]>([]);
+ const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
(async () => {
setTestingVersions(await getTestingVersions());
+ setLoading(false);
})();
}, []);
+ if (loading) {
+ return (
+ <>
+ <SteamSpinner>{t('Testing.loading')}</SteamSpinner>
+ </>
+ );
+ }
+
if (testingVersions.length === 0) {
return (
<div>
@@ -37,48 +55,54 @@ export default function TestingVersionList() {
return (
<DialogBody>
<DialogControlsSection>
+ <h4>{t('Testing.header')}</h4>
<ul style={{ listStyleType: 'none', padding: '0' }}>
{testingVersions.map((version) => {
return (
- <li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
- <span>
- {version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
- </span>
- <Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
- <DialogButton
- style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
- onClick={() => {
- downloadTestingVersion(version.id, version.head_sha);
- setSetting('branch', UpdateBranch.Testing);
- }}
- >
- <div
+ <li>
+ <Field
+ label={
+ <>
+ {version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
+ </>
+ }
+ >
+ <Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
+ <DialogButton
+ style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
+ onClick={() => {
+ downloadTestingVersion(version.id, version.head_sha);
+ setSetting('branch', UpdateBranch.Testing);
+ }}
+ >
+ <div
+ style={{
+ display: 'flex',
+ minWidth: '150px',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ }}
+ >
+ {t('Testing.download')}
+ <FaDownload style={{ paddingLeft: '1rem' }} />
+ </div>
+ </DialogButton>
+ <DialogButton
style={{
+ height: '40px',
+ width: '40px',
+ padding: '10px 12px',
+ minWidth: '40px',
display: 'flex',
- minWidth: '150px',
- justifyContent: 'space-between',
- alignItems: 'center',
+ flexDirection: 'column',
+ justifyContent: 'center',
}}
+ onClick={() => Navigation.NavigateToExternalWeb(version.link)}
>
- {t('Testing.download')}
- <FaDownload style={{ paddingLeft: '1rem' }} />
- </div>
- </DialogButton>
- <DialogButton
- style={{
- height: '40px',
- width: '40px',
- padding: '10px 12px',
- minWidth: '40px',
- display: 'flex',
- flexDirection: 'column',
- justifyContent: 'center',
- }}
- onClick={() => Navigation.NavigateToExternalWeb(version.link)}
- >
- <FaInfo />
- </DialogButton>
- </Focusable>
+ <FaInfo />
+ </DialogButton>
+ </Focusable>
+ </Field>
</li>
);
})}
diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx
index e3d1b0f1..fdb871b0 100644
--- a/frontend/src/components/store/Store.tsx
+++ b/frontend/src/components/store/Store.tsx
@@ -89,7 +89,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
useEffect(() => {
(async () => {
const res = await getPluginList(selectedSort[0], selectedSort[1]);
- logger.log('got data!', res);
+ logger.debug('got data!', res);
setPluginList(res);
setPluginCount(res.length);
})();
@@ -98,7 +98,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
useEffect(() => {
(async () => {
const storeRes = await getStore();
- logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
+ logger.debug(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
setIsTesting(storeRes === Store.Testing);
})();
}, []);
diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx
index 43073385..fd5762ea 100644
--- a/frontend/src/plugin-loader.tsx
+++ b/frontend/src/plugin-loader.tsx
@@ -2,7 +2,6 @@ import {
ModalRoot,
PanelSection,
PanelSectionRow,
- Patch,
QuickAccessTab,
Router,
findSP,
@@ -26,7 +25,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { NotificationService } from './notification-service';
-import { InstallType, Plugin } from './plugin';
+import { InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store';
@@ -41,6 +40,18 @@ const SettingsPage = lazy(() => import('./components/settings'));
const FilePicker = lazy(() => import('./components/modals/filepicker'));
+declare global {
+ interface Window {
+ __DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit?: {
+ connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this.
+ };
+ }
+}
+
+const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>(
+ 'loader/call_plugin_method',
+);
+
class PluginLoader extends Logger {
private plugins: Plugin[] = [];
private tabsHook: TabsHook = new TabsHook();
@@ -55,11 +66,21 @@ class PluginLoader extends Logger {
private reloadLock: boolean = false;
// stores a list of plugin names which requested to be reloaded
private pluginReloadQueue: { name: string; version?: string }[] = [];
-
- private focusWorkaroundPatch?: Patch;
+ private apiKeys: Map<string, string> = new Map();
constructor() {
super(PluginLoader.name);
+ console.log(import.meta.url);
+
+ DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
+ DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
+ DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
+ DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
+ DeckyBackend.addEventListener(
+ 'loader/add_multiple_plugins_install_prompt',
+ this.addMultiplePluginsInstallPrompt.bind(this),
+ );
+
this.tabsHook.init();
const TabBadge = () => {
@@ -108,7 +129,10 @@ class PluginLoader extends Logger {
.then(() => this.log('Initialized'));
}
- private getPluginsFromBackend = DeckyBackend.callable<[], { name: string; version: string }[]>('loader/get_plugins');
+ private getPluginsFromBackend = DeckyBackend.callable<
+ [],
+ { name: string; version: string; load_type: PluginLoadType }[]
+ >('loader/get_plugins');
private async loadPlugins() {
// wait for SP window to exist before loading plugins
@@ -119,7 +143,8 @@ class PluginLoader extends Logger {
const pluginLoadPromises = [];
const loadStart = performance.now();
for (const plugin of plugins) {
- if (!this.hasPlugin(plugin.name)) pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, false));
+ if (!this.hasPlugin(plugin.name))
+ pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
}
await Promise.all(pluginLoadPromises);
const loadEnd = performance.now();
@@ -256,7 +281,6 @@ class PluginLoader extends Logger {
this.routerHook.removeRoute('/decky/settings');
deinitSteamFixes();
deinitFilepickerPatches();
- this.focusWorkaroundPatch?.unpatch();
}
public unloadPlugin(name: string) {
@@ -266,7 +290,12 @@ class PluginLoader extends Logger {
this.deckyState.setPlugins(this.plugins);
}
- public async importPlugin(name: string, version?: string | undefined, useQueue: boolean = true) {
+ public async importPlugin(
+ name: string,
+ version?: string | undefined,
+ loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
+ useQueue: boolean = true,
+ ) {
if (useQueue && this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
this.pluginReloadQueue.push({ name, version: version });
@@ -279,7 +308,7 @@ class PluginLoader extends Logger {
this.unloadPlugin(name);
const startTime = performance.now();
- await this.importReactPlugin(name, version);
+ await this.importReactPlugin(name, version, loadType);
const endTime = performance.now();
this.deckyState.setPlugins(this.plugins);
@@ -297,70 +326,94 @@ class PluginLoader extends Logger {
}
}
- private async importReactPlugin(name: string, version?: string) {
- let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
- credentials: 'include',
- headers: {
- Authentication: deckyAuthToken,
- },
- });
-
- if (res.ok) {
- try {
- let plugin_export = await eval(await res.text());
- let plugin = plugin_export(this.createPluginAPI(name));
- this.plugins.push({
- ...plugin,
- name: name,
- version: version,
- });
- } catch (e) {
- this.error('Error loading plugin ' + name, e);
- const TheError: FC<{}> = () => (
- <PanelSection>
- <PanelSectionRow>
- <div
- className={quickAccessMenuClasses.FriendsTitle}
- style={{ display: 'flex', justifyContent: 'center' }}
- >
- <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
- </div>
- </PanelSectionRow>
- <PanelSectionRow>
- <pre style={{ overflowX: 'scroll' }}>
- <code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
- </pre>
- </PanelSectionRow>
- <PanelSectionRow>
- <div className={quickAccessMenuClasses.Text}>
- <TranslationHelper
- trans_class={TranslationClass.PLUGIN_LOADER}
- trans_text="plugin_error_uninstall"
- i18n_args={{ name: name }}
- />
- </div>
- </PanelSectionRow>
- </PanelSection>
- );
- this.plugins.push({
- name: name,
- version: version,
- content: <TheError />,
- icon: <FaExclamationCircle />,
- });
- this.toaster.toast({
- title: (
- <TranslationHelper
- trans_class={TranslationClass.PLUGIN_LOADER}
- trans_text="plugin_load_error.toast"
- i18n_args={{ name: name }}
- />
- ),
- body: '' + e,
- icon: <FaExclamationCircle />,
- });
+ private async importReactPlugin(
+ name: string,
+ version?: string,
+ loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
+ ) {
+ try {
+ switch (loadType) {
+ case PluginLoadType.ESMODULE_V1:
+ const uuid = this.initPluginBackendAPIConnection(name);
+ let plugin_export: () => Plugin;
+ try {
+ plugin_export = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js#apiKey=${uuid}`);
+ } finally {
+ this.destroyPluginBackendAPIConnection(uuid);
+ }
+ let plugin = plugin_export();
+
+ this.plugins.push({
+ ...plugin,
+ name: name,
+ version: version,
+ });
+ break;
+
+ case PluginLoadType.LEGACY_EVAL_IIFE:
+ let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
+ credentials: 'include',
+ headers: {
+ Authentication: deckyAuthToken,
+ },
+ });
+ if (res.ok) {
+ let plugin_export: (serverAPI: any) => Plugin = await eval(await res.text());
+ let plugin = plugin_export(this.createLegacyPluginAPI(name));
+ this.plugins.push({
+ ...plugin,
+ name: name,
+ version: version,
+ });
+ } else throw new Error(`${name} frontend_bundle not OK`);
+ break;
+
+ default:
+ throw new Error(`${name} has no defined loadType.`);
}
- } else throw new Error(`${name} frontend_bundle not OK`);
+ } catch (e) {
+ this.error('Error loading plugin ' + name, e);
+ const TheError: FC<{}> = () => (
+ <PanelSection>
+ <PanelSectionRow>
+ <div className={quickAccessMenuClasses.FriendsTitle} style={{ display: 'flex', justifyContent: 'center' }}>
+ <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
+ </div>
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <pre style={{ overflowX: 'scroll' }}>
+ <code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
+ </pre>
+ </PanelSectionRow>
+ <PanelSectionRow>
+ <div className={quickAccessMenuClasses.Text}>
+ <TranslationHelper
+ trans_class={TranslationClass.PLUGIN_LOADER}
+ trans_text="plugin_error_uninstall"
+ i18n_args={{ name: name }}
+ />
+ </div>
+ </PanelSectionRow>
+ </PanelSection>
+ );
+ this.plugins.push({
+ name: name,
+ version: version,
+ content: <TheError />,
+ icon: <FaExclamationCircle />,
+ });
+ this.toaster.toast({
+ title: (
+ <TranslationHelper
+ trans_class={TranslationClass.PLUGIN_LOADER}
+ trans_text="plugin_load_error.toast"
+ i18n_args={{ name: name }}
+ />
+ ),
+ body: '' + e,
+ icon: <FaExclamationCircle />,
+ });
+ }
}
async callServerMethod(methodName: string, args = {}) {
@@ -374,20 +427,20 @@ class PluginLoader extends Logger {
);
}
- openFilePicker(
+ openFilePickerLegacy(
startPath: string,
selectFiles?: boolean,
regex?: RegExp,
): Promise<{ path: string; realpath: string }> {
this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
if (selectFiles) {
- return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
+ return this.openFilePicker(FileSelectionType.FILE, startPath, true, true, regex);
} else {
- return this.openFilePickerV2(FileSelectionType.FOLDER, startPath, false, true, regex);
+ return this.openFilePicker(FileSelectionType.FOLDER, startPath, false, true, regex);
}
}
- openFilePickerV2(
+ openFilePicker(
select: FileSelectionType,
startPath: string,
includeFiles?: boolean,
@@ -428,27 +481,84 @@ class PluginLoader extends Logger {
});
}
- createPluginAPI(pluginName: string) {
- const pluginAPI = {
- backend: {
- call<Args extends any[] = any[], Return = void>(method: string, ...args: Args): Promise<Return> {
- return DeckyBackend.call<[pluginName: string, method: string, ...args: Args], Return>(
- 'loader/call_plugin_method',
- pluginName,
- method,
- ...args,
- );
- },
- callable<Args extends any[] = any[], Return = void>(method: string): (...args: Args) => Promise<Return> {
- return (...args) => pluginAPI.backend.call<Args, Return>(method, ...args);
- },
+ /* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
+ frontend --request URL only--> backend (ws method)
+ backend --new temporary backend URL--> frontend (ws response)
+ frontend <--> backend <--> target URL (over http!)
+ */
+ async fetchNoCors(url: string, request: any = {}) {
+ let method: string;
+ const req = { headers: {}, ...request, data: request.body };
+ req?.body && delete req.body;
+ if (!request.method) {
+ method = 'POST';
+ } else {
+ method = request.method;
+ delete req.method;
+ }
+ // this is terrible but a. we're going to redo this entire method anyway and b. it was already terrible
+ try {
+ const ret = await DeckyBackend.call<
+ [method: string, url: string, extra_opts?: any],
+ { status: number; headers: { [key: string]: string }; body: string }
+ >('utilities/http_request', method, url, req);
+ return { success: true, result: ret };
+ } catch (e) {
+ return { success: false, result: e?.toString() };
+ }
+ }
+
+ destroyPluginBackendAPIConnection(uuid: string) {
+ if (this.apiKeys.delete(uuid)) {
+ this.debug(`backend api connection init data destroyed for ${uuid}`);
+ }
+ }
+
+ initPluginBackendAPI() {
+ // Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear.
+ window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit = {
+ connect: (version: number, key: string) => {
+ if (!this.apiKeys.has(key)) {
+ throw new Error(`Backend API key ${key} is invalid.`);
+ }
+
+ const pluginName = this.apiKeys.get(key)!;
+
+ if (version <= 0) {
+ this.destroyPluginBackendAPIConnection(key);
+ throw new Error(`UUID ${key} requested invalid backend api version ${version}.`);
+ }
+
+ const backendAPI = {
+ call: (methodName: string, ...args: any) => {
+ return callPluginMethod(pluginName, methodName, ...args);
+ },
+ callable: (methodName: string) => {
+ return (...args: any) => callPluginMethod(pluginName, methodName, ...args);
+ },
+ };
+
+ this.destroyPluginBackendAPIConnection(key);
+ return backendAPI;
},
+ };
+ }
+
+ initPluginBackendAPIConnection(pluginName: string) {
+ const key = crypto.randomUUID();
+ this.apiKeys.set(key, pluginName);
+
+ return key;
+ }
+
+ createLegacyPluginAPI(pluginName: string) {
+ const pluginAPI = {
routerHook: this.routerHook,
toaster: this.toaster,
// Legacy
callServerMethod: this.callServerMethod,
- openFilePicker: this.openFilePicker,
- openFilePickerV2: this.openFilePickerV2,
+ openFilePicker: this.openFilePickerLegacy,
+ openFilePickerV2: this.openFilePicker,
// Legacy
async callPluginMethod(methodName: string, args = {}) {
return DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
@@ -458,32 +568,7 @@ class PluginLoader extends Logger {
args,
);
},
- /* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
- frontend --request URL only--> backend (ws method)
- backend --new temporary backend URL--> frontend (ws response)
- frontend <--> backend <--> target URL (over http!)
- */
- async fetchNoCors(url: string, request: any = {}) {
- let method: string;
- const req = { headers: {}, ...request, data: request.body };
- req?.body && delete req.body;
- if (!request.method) {
- method = 'POST';
- } else {
- method = request.method;
- delete req.method;
- }
- // this is terrible but a. we're going to redo this entire method anyway and b. it was already terrible
- try {
- const ret = await DeckyBackend.call<
- [method: string, url: string, extra_opts?: any],
- { status: number; headers: { [key: string]: string }; body: string }
- >('utilities/http_request', method, url, req);
- return { success: true, result: ret };
- } catch (e) {
- return { success: false, result: e?.toString() };
- }
- },
+ fetchNoCors: this.fetchNoCors,
executeInTab: DeckyBackend.callable<
[tab: String, runAsync: Boolean, code: string],
{ success: boolean; result: any }
diff --git a/frontend/src/plugin.ts b/frontend/src/plugin.ts
index d750a63e..92a0c625 100644
--- a/frontend/src/plugin.ts
+++ b/frontend/src/plugin.ts
@@ -1,3 +1,8 @@
+export enum PluginLoadType {
+ LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
+ ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
+}
+
export interface Plugin {
name: string;
version?: string;
diff --git a/frontend/src/start.tsx b/frontend/src/start.tsx
index 0803f46e..a2837a1b 100644
--- a/frontend/src/start.tsx
+++ b/frontend/src/start.tsx
@@ -6,7 +6,6 @@ import PluginLoader from './plugin-loader';
declare global {
export var DeckyPluginLoader: PluginLoader;
- export var importDeckyPlugin: Function;
export var deckyHasLoaded: boolean;
export var deckyHasConnectedRDT: boolean | undefined;
export var deckyAuthToken: string;
@@ -45,9 +44,7 @@ declare global {
window?.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
DeckyPluginLoader.init();
- window.importDeckyPlugin = function (name: string, version: string) {
- DeckyPluginLoader?.importPlugin(name, version);
- };
+ console.log(import.meta.url);
})();
export default i18n;
diff --git a/frontend/src/steamfixes/index.ts b/frontend/src/steamfixes/index.ts
index fe0e3e05..45f07b2a 100644
--- a/frontend/src/steamfixes/index.ts
+++ b/frontend/src/steamfixes/index.ts
@@ -1,5 +1,5 @@
-import reloadFix from './reload';
-import restartFix from './restart';
+// import reloadFix from './reload';
+// import restartFix from './restart';
let fixes: Function[] = [];
export function deinitSteamFixes() {
@@ -7,6 +7,6 @@ export function deinitSteamFixes() {
}
export async function initSteamFixes() {
- fixes.push(await reloadFix());
- fixes.push(await restartFix());
+ // fixes.push(await reloadFix());
+ // fixes.push(await restartFix());
}
diff --git a/frontend/src/updater.ts b/frontend/src/updater.ts
index edc9eeb3..c8e96521 100644
--- a/frontend/src/updater.ts
+++ b/frontend/src/updater.ts
@@ -28,6 +28,6 @@ export const doRestart = DeckyBackend.callable('updater/do_restart');
export const getVersionInfo = DeckyBackend.callable<[], VerInfo>('updater/get_version_info');
export const checkForUpdates = DeckyBackend.callable<[], VerInfo>('updater/check_for_updates');
-DeckyBackend.addEventListener('frontend/finish_download', async () => {
+DeckyBackend.addEventListener('updater/finish_download', async () => {
await doRestart();
});
diff --git a/frontend/src/wsrouter.ts b/frontend/src/wsrouter.ts
index 778c03bf..37df4262 100644
--- a/frontend/src/wsrouter.ts
+++ b/frontend/src/wsrouter.ts
@@ -50,7 +50,6 @@ interface PromiseResolver<T> {
}
export class WSRouter extends Logger {
- routes: Map<string, (...args: any) => any> = new Map();
runningCalls: Map<number, PromiseResolver<any>> = new Map();
eventListeners: Map<string, Set<(...args: any) => any>> = new Map();
ws?: WebSocket;
@@ -92,14 +91,6 @@ export class WSRouter extends Logger {
this.ws?.send(JSON.stringify(data));
}
- addRoute(name: string, route: (...args: any) => any) {
- this.routes.set(name, route);
- }
-
- removeRoute(name: string) {
- this.routes.delete(name);
- }
-
addEventListener(event: string, listener: (...args: any) => any) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set([listener]));
@@ -123,20 +114,6 @@ export class WSRouter extends Logger {
try {
const data = JSON.parse(msg.data) as Message;
switch (data.type) {
- case MessageType.CALL:
- if (this.routes.has(data.route)) {
- try {
- const res = await this.routes.get(data.route)!(...data.args);
- this.write({ type: MessageType.REPLY, id: data.id, result: res });
- this.debug(`Started JS call ${data.route} ID ${data.id}`);
- } catch (e) {
- await this.write({ type: MessageType.ERROR, id: data.id, error: (e as Error)?.stack || e });
- }
- } else {
- await this.write({ type: MessageType.ERROR, id: data.id, error: `Route ${data.route} does not exist.` });
- }
- break;
-
case MessageType.REPLY:
if (this.runningCalls.has(data.id)) {
this.runningCalls.get(data.id)!.resolve(data.result);
@@ -154,6 +131,7 @@ export class WSRouter extends Logger {
break;
case MessageType.EVENT:
+ this.debug(`Recieved event ${data.event} with args`, data.args);
if (this.eventListeners.has(data.event)) {
for (const listener of this.eventListeners.get(data.event)!) {
(async () => {