2
0
Эх сурвалжийг харах

refactor: Consolidated and separated CustomWebSocket

Owen Diffey 2 жил өмнө
parent
commit
95aa420502
35 өөрчлөгдсөн 367 нэмэгдсэн , 374 устгасан
  1. 2 3
      frontend/src/App.vue
  2. 202 0
      frontend/src/classes/CustomWebSocket.class.ts
  3. 1 2
      frontend/src/components/AddToPlaylistDropdown.vue
  4. 1 2
      frontend/src/components/AdvancedTable.vue
  5. 1 2
      frontend/src/components/PlaylistTabBase.vue
  6. 1 2
      frontend/src/components/modals/BulkActions.vue
  7. 1 2
      frontend/src/components/modals/EditNews.vue
  8. 1 2
      frontend/src/components/modals/EditPlaylist/index.vue
  9. 1 2
      frontend/src/components/modals/EditSong/index.vue
  10. 1 2
      frontend/src/components/modals/EditUser.vue
  11. 1 2
      frontend/src/components/modals/ImportAlbum.vue
  12. 1 2
      frontend/src/components/modals/Report.vue
  13. 1 2
      frontend/src/components/modals/ViewApiRequest.vue
  14. 1 2
      frontend/src/components/modals/ViewPunishment.vue
  15. 1 2
      frontend/src/components/modals/ViewReport.vue
  16. 1 2
      frontend/src/components/modals/ViewYoutubeVideo.vue
  17. 5 3
      frontend/src/composables/useReports.ts
  18. 1 2
      frontend/src/composables/useSortablePlaylists.ts
  19. 64 62
      frontend/src/main.ts
  20. 1 2
      frontend/src/pages/Admin/Statistics.vue
  21. 1 2
      frontend/src/pages/Admin/YouTube/index.vue
  22. 1 2
      frontend/src/pages/Home.vue
  23. 1 3
      frontend/src/pages/News.vue
  24. 1 2
      frontend/src/pages/Profile/Tabs/RecentActivity.vue
  25. 1 2
      frontend/src/pages/Profile/index.vue
  26. 1 2
      frontend/src/pages/Settings/Tabs/Preferences.vue
  27. 1 2
      frontend/src/pages/Settings/index.vue
  28. 2 3
      frontend/src/pages/Station/index.vue
  29. 3 2
      frontend/src/stores/manageStation.ts
  30. 7 4
      frontend/src/stores/modals.ts
  31. 3 2
      frontend/src/stores/station.ts
  32. 29 27
      frontend/src/stores/userAuth.ts
  33. 27 17
      frontend/src/stores/websockets.ts
  34. 0 7
      frontend/src/types/customWebSocket.ts
  35. 0 197
      frontend/src/ws.ts

+ 2 - 3
frontend/src/App.vue

@@ -7,7 +7,6 @@ import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
 import aw from "@/aw";
 import keyboardShortcuts from "@/keyboardShortcuts";
 
@@ -179,7 +178,7 @@ onMounted(async () => {
 
 	disconnectedMessage.value.hide();
 
-	ws.onConnect(() => {
+	socket.onConnect(() => {
 		socketConnected.value = true;
 
 		socket.dispatch("users.getPreferences", res => {
@@ -239,7 +238,7 @@ onMounted(async () => {
 		});
 	});
 
-	ws.onDisconnect(true, () => {
+	socket.onDisconnect(true, () => {
 		socketConnected.value = false;
 	});
 

+ 202 - 0
frontend/src/classes/CustomWebSocket.class.ts

@@ -0,0 +1,202 @@
+import ListenerHandler from "@/classes/ListenerHandler.class";
+import { useUserAuthStore } from "@/stores/userAuth";
+
+export default class CustomWebSocket extends WebSocket {
+	dispatcher: ListenerHandler;
+
+	onConnectCbs: any[];
+
+	ready: boolean;
+
+	firstInit: boolean;
+
+	pendingDispatches: any[];
+
+	onDisconnectCbs: {
+		temp: any[];
+		persist: any[];
+	};
+
+	CB_REF: number;
+
+	CB_REFS: object;
+
+	PROGRESS_CB_REFS: object;
+
+	constructor(url) {
+		super(url);
+
+		this.dispatcher = new ListenerHandler();
+
+		this.onConnectCbs = [];
+		this.ready = false;
+		this.firstInit = true;
+
+		this.pendingDispatches = [];
+
+		this.onDisconnectCbs = {
+			temp: [],
+			persist: []
+		};
+
+		// references for when a dispatch event is ready to callback from server to client
+		this.CB_REF = 0;
+		this.CB_REFS = {};
+		this.PROGRESS_CB_REFS = {};
+
+		this.init();
+	}
+
+	init() {
+		const userAuthStore = useUserAuthStore();
+
+		this.onopen = () => {
+			console.log("WS: SOCKET OPENED");
+		};
+
+		this.onmessage = message => {
+			const data = JSON.parse(message.data);
+			const name = data.shift(0);
+
+			if (name === "CB_REF") {
+				const CB_REF = data.shift(0);
+				this.CB_REFS[CB_REF](...data);
+				return delete this.CB_REFS[CB_REF];
+			}
+			if (name === "PROGRESS_CB_REF") {
+				const PROGRESS_CB_REF = data.shift(0);
+				this.PROGRESS_CB_REFS[PROGRESS_CB_REF](...data);
+			}
+
+			if (name === "ERROR") console.log("WS: SOCKET ERROR:", data[0]);
+
+			return this.dispatcher.dispatchEvent(
+				new CustomEvent(name, {
+					detail: data
+				})
+			);
+		};
+
+		this.onclose = () => {
+			console.log("WS: SOCKET CLOSED");
+
+			this.ready = false;
+
+			this.onDisconnectCbs.temp.forEach(cb => cb());
+			this.onDisconnectCbs.persist.forEach(cb => cb());
+
+			// try to reconnect every 1000ms, if the user isn't banned
+			if (!userAuthStore.banned) setTimeout(() => this.init(), 1000);
+		};
+
+		this.onerror = err => {
+			console.log("WS: SOCKET ERROR", err);
+		};
+
+		if (this.firstInit) {
+			this.firstInit = false;
+			this.on("ready", () => {
+				console.log("WS: SOCKET READY");
+
+				this.onConnectCbs.forEach(cb => cb());
+
+				this.ready = true;
+
+				setTimeout(() => {
+					// dispatches that were attempted while the server was offline
+					this.pendingDispatches.forEach(cb => cb());
+					this.pendingDispatches = [];
+				}, 150); // small delay between readyState being 1 and the server actually receiving dispatches
+
+				userAuthStore.updatePermissions();
+			});
+		}
+	}
+
+	on(target, cb, options?) {
+		this.dispatcher.addEventListener(
+			target,
+			event => cb(...event.detail),
+			options
+		);
+	}
+
+	dispatch(...args) {
+		if (this.readyState !== 1)
+			return this.pendingDispatches.push(() => this.dispatch(...args));
+
+		const lastArg = args[args.length - 1];
+
+		if (typeof lastArg === "function") {
+			this.CB_REF += 1;
+			this.CB_REFS[this.CB_REF] = lastArg;
+
+			return this.send(
+				JSON.stringify([...args.slice(0, -1), { CB_REF: this.CB_REF }])
+			);
+		}
+		if (typeof lastArg === "object") {
+			this.CB_REF += 1;
+			this.CB_REFS[this.CB_REF] = lastArg.cb;
+			this.PROGRESS_CB_REFS[this.CB_REF] = lastArg.onProgress;
+
+			return this.send(
+				JSON.stringify([
+					...args.slice(0, -1),
+					{ CB_REF: this.CB_REF, onProgress: true }
+				])
+			);
+		}
+
+		return this.send(JSON.stringify([...args]));
+	}
+
+	onConnect(cb) {
+		if (this.readyState === 1 && this.ready) cb();
+
+		return this.onConnectCbs.push(cb);
+	}
+
+	onDisconnect(...args) {
+		if (args[0] === true) this.onDisconnectCbs.persist.push(args[1]);
+		else this.onDisconnectCbs.temp.push(args[0]);
+	}
+
+	clearCallbacks() {
+		this.onDisconnectCbs.temp = [];
+	}
+
+	destroyListeners() {
+		Object.keys(this.CB_REFS).forEach(id => {
+			if (
+				id.indexOf("$event:") !== -1 &&
+				id.indexOf("$event:keep.") === -1
+			)
+				delete this.CB_REFS[id];
+		});
+
+		Object.keys(this.PROGRESS_CB_REFS).forEach(id => {
+			if (
+				id.indexOf("$event:") !== -1 &&
+				id.indexOf("$event:keep.") === -1
+			)
+				delete this.PROGRESS_CB_REFS[id];
+		});
+
+		// destroy all listeners that aren't site-wide
+		Object.keys(this.dispatcher.listeners).forEach(type => {
+			if (type.indexOf("keep.") === -1 && type !== "ready")
+				delete this.dispatcher.listeners[type];
+		});
+	}
+
+	destroyModalListeners(modalUuid) {
+		// destroy all listeners for a specific modal
+		Object.keys(this.dispatcher.listeners).forEach(type =>
+			this.dispatcher.listeners[type].forEach((element, index) => {
+				if (element.options && element.options.modalUuid === modalUuid)
+					this.dispatcher.listeners[type].splice(index, 1);
+			})
+		);
+	}
+}

+ 1 - 2
frontend/src/components/AddToPlaylistDropdown.vue

@@ -5,7 +5,6 @@ import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserPlaylistsStore } from "@/stores/userPlaylists";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
 
 const props = defineProps({
 	song: {
@@ -71,7 +70,7 @@ const createPlaylist = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	socket.on("event:playlist.created", res => addPlaylist(res.data.playlist), {
 		replaceable: true

+ 1 - 2
frontend/src/components/AdvancedTable.vue

@@ -17,7 +17,6 @@ import { DraggableList } from "vue-draggable-list";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import keyboardShortcuts from "@/keyboardShortcuts";
-import ws from "@/ws";
 import { useDragBox } from "@/composables/useDragBox";
 import {
 	TableColumn,
@@ -934,7 +933,7 @@ onMounted(async () => {
 		}
 	}
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	// TODO, this doesn't address special properties
 	if (props.events && props.events.updated)

+ 1 - 2
frontend/src/components/PlaylistTabBase.vue

@@ -2,7 +2,6 @@
 import { defineAsyncComponent, ref, reactive, computed, onMounted } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
-import ws from "@/ws";
 
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useStationStore } from "@/stores/station";
@@ -296,7 +295,7 @@ const searchForPlaylists = page => {
 onMounted(() => {
 	showTab("search");
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/components/modals/BulkActions.vue

@@ -6,7 +6,6 @@ import { useWebsocketsStore } from "@/stores/websockets";
 import { useLongJobsStore } from "@/stores/longJobs";
 import { useBulkActionsStore } from "@/stores/bulkActions";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const AutoSuggest = defineAsyncComponent(
@@ -96,7 +95,7 @@ onBeforeUnmount(() => {
 });
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/components/modals/EditNews.vue

@@ -8,7 +8,6 @@ import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useEditNewsStore } from "@/stores/editNews";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const SaveButton = defineAsyncComponent(
@@ -142,7 +141,7 @@ onMounted(() => {
 		}
 	});
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/components/modals/EditPlaylist/index.vue

@@ -14,7 +14,6 @@ import { useEditPlaylistStore } from "@/stores/editPlaylist";
 import { useStationStore } from "@/stores/station";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
 import utils from "@/utils";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
@@ -257,7 +256,7 @@ const clearAndRefillGenrePlaylist = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	socket.on(
 		"event:playlist.song.added",

+ 1 - 2
frontend/src/components/modals/EditSong/index.vue

@@ -10,7 +10,6 @@ import {
 } from "vue";
 import Toast from "toasters";
 import aw from "@/aw";
-import ws from "@/ws";
 import validation from "@/validation";
 import keyboardShortcuts from "@/keyboardShortcuts";
 
@@ -1220,7 +1219,7 @@ onMounted(async () => {
 
 	useHTTPS.value = await lofig.get("cookie.secure");
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	let volume = parseFloat(localStorage.getItem("volume"));
 	volume = typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;

+ 1 - 2
frontend/src/components/modals/EditUser.vue

@@ -8,7 +8,6 @@ import {
 } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
-import ws from "@/ws";
 import validation from "@/validation";
 import { useEditUserStore } from "@/stores/editUser";
 import { useWebsocketsStore } from "@/stores/websockets";
@@ -166,7 +165,7 @@ watch(
 );
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 
 onBeforeUnmount(() => {

+ 1 - 2
frontend/src/components/modals/ImportAlbum.vue

@@ -12,7 +12,6 @@ import { DraggableList } from "vue-draggable-list";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useImportAlbumStore } from "@/stores/importAlbum";
-import ws from "@/ws";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const SongItem = defineAsyncComponent(
@@ -332,7 +331,7 @@ const updateTrackSong = updatedSong => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	socket.on("event:admin.song.updated", res => {
 		updateTrackSong(res.data.song);

+ 1 - 2
frontend/src/components/modals/Report.vue

@@ -5,7 +5,6 @@ import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useReportStore } from "@/stores/report";
-import ws from "@/ws";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const SongItem = defineAsyncComponent(
@@ -204,7 +203,7 @@ const create = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 
 onBeforeUnmount(() => {

+ 1 - 2
frontend/src/components/modals/ViewApiRequest.vue

@@ -6,7 +6,6 @@ import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useViewApiRequestStore } from "@/stores/viewApiRequest";
-import ws from "@/ws";
 import "vue-json-pretty/lib/styles.css";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
@@ -69,7 +68,7 @@ const remove = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 
 onBeforeUnmount(() => {

+ 1 - 2
frontend/src/components/modals/ViewPunishment.vue

@@ -5,7 +5,6 @@ import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useViewPunishmentStore } from "@/stores/viewPunishment";
-import ws from "@/ws";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 const PunishmentItem = defineAsyncComponent(
@@ -64,7 +63,7 @@ const deactivatePunishment = event => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 
 onBeforeUnmount(() => {

+ 1 - 2
frontend/src/components/modals/ViewReport.vue

@@ -13,7 +13,6 @@ import { useModalsStore } from "@/stores/modals";
 import { useViewReportStore } from "@/stores/viewReport";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useReports } from "@/composables/useReports";
-import ws from "@/ws";
 import { Report } from "@/types/report";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
@@ -140,7 +139,7 @@ watch(
 );
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 
 onBeforeUnmount(() => {

+ 1 - 2
frontend/src/components/modals/ViewYoutubeVideo.vue

@@ -3,7 +3,6 @@ import { defineAsyncComponent, onMounted, onBeforeUnmount, ref } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import aw from "@/aw";
-import ws from "@/ws";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
 import { useViewYoutubeVideoStore } from "@/stores/viewYoutubeVideo";
@@ -457,7 +456,7 @@ const init = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 
 onBeforeUnmount(() => {

+ 5 - 3
frontend/src/composables/useReports.ts

@@ -1,10 +1,12 @@
 import Toast from "toasters";
-import ws from "@/ws";
+import { useWebsocketsStore } from "@/stores/websockets";
 
 export const useReports = () => {
+	const { socket } = useWebsocketsStore();
+
 	const resolveReport = ({ reportId, value }) =>
 		new Promise((resolve, reject) => {
-			ws.socket.dispatch("reports.resolve", reportId, value, res => {
+			socket.dispatch("reports.resolve", reportId, value, res => {
 				new Toast(res.message);
 				if (res.status === "success")
 					return resolve({ status: "success" });
@@ -14,7 +16,7 @@ export const useReports = () => {
 
 	const removeReport = reportId =>
 		new Promise((resolve, reject) => {
-			ws.socket.dispatch("reports.remove", reportId, res => {
+			socket.dispatch("reports.remove", reportId, res => {
 				new Toast(res.message);
 				if (res.status === "success")
 					return resolve({ status: "success" });

+ 1 - 2
frontend/src/composables/useSortablePlaylists.ts

@@ -5,7 +5,6 @@ import { DraggableList } from "vue-draggable-list";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPlaylistsStore } from "@/stores/userPlaylists";
-import ws from "@/ws";
 
 export const useSortablePlaylists = () => {
 	const orderOfPlaylists = ref([]);
@@ -61,7 +60,7 @@ export const useSortablePlaylists = () => {
 
 		if (!userId.value) userId.value = myUserId.value;
 
-		ws.onConnect(() => {
+		socket.onConnect(() => {
 			if (!isCurrentUser.value)
 				socket.dispatch(
 					"apis.joinRoom",

+ 64 - 62
frontend/src/main.ts

@@ -10,7 +10,7 @@ import Toast from "toasters";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
+import { useWebsocketsStore } from "@/stores/websockets";
 import ms from "@/ms";
 import i18n from "@/i18n";
 
@@ -262,9 +262,11 @@ router.beforeEach((to, from, next) => {
 
 	modalsStore.closeAllModals();
 
-	if (ws.socket && to.fullPath !== from.fullPath) {
-		ws.clearCallbacks();
-		ws.destroyListeners();
+	const { socket } = useWebsocketsStore();
+
+	if (socket.ready && to.fullPath !== from.fullPath) {
+		socket.clearCallbacks();
+		socket.destroyListeners();
 	}
 
 	if (to.query.toast) {
@@ -337,77 +339,77 @@ lofig.folder = defaultConfigURL;
 		}
 	});
 
-	const websocketsDomain = await lofig.get("backend.websocketsDomain");
-	ws.init(websocketsDomain);
-
-	if (await lofig.get("siteSettings.mediasession")) ms.init();
-
-	ws.socket.on("ready", res => {
-		const { loggedIn, role, username, userId, email } = res.data;
-
-		userAuthStore.authData({
-			loggedIn,
-			role,
-			username,
-			email,
-			userId
+	const { createSocket } = useWebsocketsStore();
+	createSocket().then(socket => {
+		socket.on("ready", res => {
+			const { loggedIn, role, username, userId, email } = res.data;
+
+			userAuthStore.authData({
+				loggedIn,
+				role,
+				username,
+				email,
+				userId
+			});
 		});
-	});
 
-	ws.socket.on("keep.event:user.banned", res =>
-		userAuthStore.banUser(res.data.ban)
-	);
+		socket.on("keep.event:user.banned", res =>
+			userAuthStore.banUser(res.data.ban)
+		);
 
-	ws.socket.on("keep.event:user.username.updated", res =>
-		userAuthStore.updateUsername(res.data.username)
-	);
+		socket.on("keep.event:user.username.updated", res =>
+			userAuthStore.updateUsername(res.data.username)
+		);
 
-	ws.socket.on("keep.event:user.preferences.updated", res => {
-		const { preferences } = res.data;
+		socket.on("keep.event:user.preferences.updated", res => {
+			const { preferences } = res.data;
 
-		const {
-			changeAutoSkipDisliked,
-			changeNightmode,
-			changeActivityLogPublic,
-			changeAnonymousSongRequests,
-			changeActivityWatch
-		} = useUserPreferencesStore();
+			const {
+				changeAutoSkipDisliked,
+				changeNightmode,
+				changeActivityLogPublic,
+				changeAnonymousSongRequests,
+				changeActivityWatch
+			} = useUserPreferencesStore();
 
-		if (preferences.autoSkipDisliked !== undefined)
-			changeAutoSkipDisliked(preferences.autoSkipDisliked);
+			if (preferences.autoSkipDisliked !== undefined)
+				changeAutoSkipDisliked(preferences.autoSkipDisliked);
 
-		if (preferences.nightmode !== undefined) {
-			localStorage.setItem("nightmode", preferences.nightmode);
-			changeNightmode(preferences.nightmode);
-		}
+			if (preferences.nightmode !== undefined) {
+				localStorage.setItem("nightmode", preferences.nightmode);
+				changeNightmode(preferences.nightmode);
+			}
 
-		if (preferences.activityLogPublic !== undefined)
-			changeActivityLogPublic(preferences.activityLogPublic);
+			if (preferences.activityLogPublic !== undefined)
+				changeActivityLogPublic(preferences.activityLogPublic);
 
-		if (preferences.anonymousSongRequests !== undefined)
-			changeAnonymousSongRequests(preferences.anonymousSongRequests);
+			if (preferences.anonymousSongRequests !== undefined)
+				changeAnonymousSongRequests(preferences.anonymousSongRequests);
 
-		if (preferences.activityWatch !== undefined)
-			changeActivityWatch(preferences.activityWatch);
-	});
+			if (preferences.activityWatch !== undefined)
+				changeActivityWatch(preferences.activityWatch);
+		});
 
-	ws.socket.on("keep.event:user.role.updated", res => {
-		userAuthStore.updateRole(res.data.role);
-		userAuthStore.updatePermissions().then(() => {
-			const { meta } = router.currentRoute.value;
-			if (
-				meta &&
-				meta.permissionRequired &&
-				!userAuthStore.hasPermission(meta.permissionRequired)
-			)
-				router.push({
-					path: "/",
-					query: {
-						toast: "You no longer have access to the page you were viewing."
-					}
-				});
+		socket.on("keep.event:user.role.updated", res => {
+			userAuthStore.updateRole(res.data.role);
+			userAuthStore.updatePermissions().then(() => {
+				const { meta } = router.currentRoute.value;
+				if (
+					meta &&
+					meta.permissionRequired &&
+					!userAuthStore.hasPermission(meta.permissionRequired)
+				)
+					router.push({
+						path: "/",
+						query: {
+							toast: "You no longer have access to the page you were viewing."
+						}
+					});
+			});
 		});
 	});
 
+	if (await lofig.get("siteSettings.mediasession")) ms.init();
+
 	app.mount("#root");
 })();

+ 1 - 2
frontend/src/pages/Admin/Statistics.vue

@@ -2,7 +2,6 @@
 import { ref, onMounted } from "vue";
 import { useRoute } from "vue-router";
 import { useWebsocketsStore } from "@/stores/websockets";
-import ws from "@/ws";
 
 const route = useRoute();
 
@@ -28,7 +27,7 @@ const init = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/pages/Admin/YouTube/index.vue

@@ -4,7 +4,6 @@ import { useRoute } from "vue-router";
 import Toast from "toasters";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
-import ws from "@/ws";
 import { TableColumn, TableFilter, TableEvents } from "@/types/advancedTable";
 
 const AdvancedTable = defineAsyncComponent(
@@ -193,7 +192,7 @@ const removeApiRequest = requestId => {
 };
 
 onMounted(() => {
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/pages/Home.vue

@@ -16,7 +16,6 @@ import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
 import keyboardShortcuts from "@/keyboardShortcuts";
-import ws from "@/ws";
 
 const MainHeader = defineAsyncComponent(
 	() => import("@/components/MainHeader.vue")
@@ -192,7 +191,7 @@ onMounted(async () => {
 		openModal(route.redirectedFrom.name);
 	}
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	socket.on("event:station.created", res => {
 		const { station } = res.data;

+ 1 - 3
frontend/src/pages/News.vue

@@ -6,8 +6,6 @@ import { marked } from "marked";
 import DOMPurify from "dompurify";
 import { useWebsocketsStore } from "@/stores/websockets";
 
-import ws from "@/ws";
-
 const MainHeader = defineAsyncComponent(
 	() => import("@/components/MainHeader.vue")
 );
@@ -67,7 +65,7 @@ onMounted(() => {
 		news.value = news.value.filter(item => item._id !== res.data.newsId);
 	});
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/pages/Profile/Tabs/RecentActivity.vue

@@ -4,7 +4,6 @@ import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
-import ws from "@/ws";
 
 const ActivityItem = defineAsyncComponent(
 	() => import("@/components/ActivityItem.vue")
@@ -87,7 +86,7 @@ const handleScroll = () => {
 onMounted(() => {
 	window.addEventListener("scroll", handleScroll);
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	socket.on("event:activity.updated", res => {
 		activities.value.find(

+ 1 - 2
frontend/src/pages/Profile/index.vue

@@ -5,7 +5,6 @@ import { format, parseISO } from "date-fns";
 import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
-import ws from "@/ws";
 import { useTabQueryHandler } from "@/composables/useTabQueryHandler";
 
 const MainHeader = defineAsyncComponent(
@@ -60,7 +59,7 @@ onMounted(() => {
 	)
 		tab.value = route.query.tab;
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 });
 </script>
 

+ 1 - 2
frontend/src/pages/Settings/Tabs/Preferences.vue

@@ -4,7 +4,6 @@ import Toast from "toasters";
 import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserPreferencesStore } from "@/stores/userPreferences";
-import ws from "@/ws";
 
 const SaveButton = defineAsyncComponent(
 	() => import("@/components/SaveButton.vue")
@@ -66,7 +65,7 @@ const saveChanges = () => {
 };
 
 onMounted(() => {
-	ws.onConnect(() =>
+	socket.onConnect(() =>
 		socket.dispatch("users.getPreferences", res => {
 			const { preferences } = res.data;
 

+ 1 - 2
frontend/src/pages/Settings/index.vue

@@ -4,7 +4,6 @@ import { onMounted, defineAsyncComponent } from "vue";
 import Toast from "toasters";
 import { useSettingsStore } from "@/stores/settings";
 import { useWebsocketsStore } from "@/stores/websockets";
-import ws from "@/ws";
 import { useTabQueryHandler } from "@/composables/useTabQueryHandler";
 
 const MainHeader = defineAsyncComponent(
@@ -53,7 +52,7 @@ onMounted(() => {
 
 	// this.localNightmode = this.nightmode;
 
-	ws.onConnect(init);
+	socket.onConnect(init);
 
 	socket.on("event:user.password.linked", () =>
 		updateOriginalUser({

+ 2 - 3
frontend/src/pages/Station/index.vue

@@ -19,7 +19,6 @@ import { useUserPreferencesStore } from "@/stores/userPreferences";
 import { useModalsStore } from "@/stores/modals";
 import aw from "@/aw";
 import ms from "@/ms";
-import ws from "@/ws";
 import keyboardShortcuts from "@/keyboardShortcuts";
 import utils from "@/utils";
 
@@ -1121,13 +1120,13 @@ onMounted(async () => {
 	}, 1000);
 
 	if (socket.readyState === 1) join();
-	ws.onConnect(() => {
+	socket.onConnect(() => {
 		socketConnected.value = true;
 		clearTimeout(window.stationNextSongTimeout);
 		join();
 	});
 
-	ws.onDisconnect(true, () => {
+	socket.onDisconnect(true, () => {
 		socketConnected.value = false;
 		const _currentSong = currentSong.value;
 		if (nextSong.value)

+ 3 - 2
frontend/src/stores/manageStation.ts

@@ -2,7 +2,7 @@ import { defineStore } from "pinia";
 import { Station } from "@/types/station";
 import { Playlist } from "@/types/playlist";
 import { CurrentSong, Song } from "@/types/song";
-import ws from "@/ws";
+import { useWebsocketsStore } from "@/stores/websockets";
 
 export const useManageStationStore = props => {
 	const { modalUuid } = props;
@@ -84,7 +84,8 @@ export const useManageStationStore = props => {
 			},
 			updatePermissions() {
 				return new Promise(resolve => {
-					ws.socket.dispatch(
+					const { socket } = useWebsocketsStore();
+					socket.dispatch(
 						"utils.getPermissions",
 						this.station._id,
 						res => {

+ 7 - 4
frontend/src/stores/modals.ts

@@ -1,7 +1,7 @@
 import { defineStore } from "pinia";
 import { defineAsyncComponent } from "vue";
-import ws from "@/ws";
 
+import { useWebsocketsStore } from "@/stores/websockets";
 import { useEditUserStore } from "@/stores/editUser";
 import { useEditSongStore } from "@/stores/editSong";
 import { useBulkActionsStore } from "@/stores/bulkActions";
@@ -33,7 +33,8 @@ export const useModalsStore = defineStore("modals", {
 
 			Object.entries(this.modals).forEach(([uuid, _modal]) => {
 				if (modal === _modal) {
-					ws.destroyModalListeners(uuid);
+					const { socket } = useWebsocketsStore();
+					socket.destroyModalListeners(uuid);
 					this.activeModals.splice(
 						this.activeModals.indexOf(uuid),
 						1
@@ -133,15 +134,17 @@ export const useModalsStore = defineStore("modals", {
 				this.activeModals[this.activeModals.length - 1];
 			// TODO: make sure to only destroy/register modal listeners for a unique modal
 			// remove any websocket listeners for the modal
-			ws.destroyModalListeners(currentlyActiveModalUuid);
+			const { socket } = useWebsocketsStore();
+			socket.destroyModalListeners(currentlyActiveModalUuid);
 
 			this.activeModals.pop();
 
 			delete this.modals[currentlyActiveModalUuid];
 		},
 		closeAllModals() {
+			const { socket } = useWebsocketsStore();
 			this.activeModals.forEach(modalUuid => {
-				ws.destroyModalListeners(modalUuid);
+				socket.destroyModalListeners(modalUuid);
 			});
 
 			this.activeModals = [];

+ 3 - 2
frontend/src/stores/station.ts

@@ -3,7 +3,7 @@ import { Playlist } from "@/types/playlist";
 import { Song, CurrentSong } from "@/types/song";
 import { Station } from "@/types/station";
 import { User } from "@/types/user";
-import ws from "@/ws";
+import { useWebsocketsStore } from "@/stores/websockets";
 
 export const useStationStore = defineStore("station", {
 	state: () => ({
@@ -141,7 +141,8 @@ export const useStationStore = defineStore("station", {
 		},
 		updatePermissions() {
 			return new Promise(resolve => {
-				ws.socket.dispatch(
+				const { socket } = useWebsocketsStore();
+				socket.dispatch(
 					"utils.getPermissions",
 					this.station._id,
 					res => {

+ 29 - 27
frontend/src/stores/userAuth.ts

@@ -1,7 +1,7 @@
 import { defineStore } from "pinia";
 import Toast from "toasters";
 import validation from "@/validation";
-import ws from "@/ws";
+import { useWebsocketsStore } from "@/stores/websockets";
 
 export const useUserAuthStore = defineStore("userAuth", {
 	state: () => ({
@@ -70,8 +70,9 @@ export const useUserAuthStore = defineStore("userAuth", {
 							"Invalid password format. Must have one lowercase letter, one uppercase letter, one number and one special character."
 						)
 					);
-				else
-					ws.socket.dispatch(
+				else {
+					const { socket } = useWebsocketsStore();
+					socket.dispatch(
 						"users.register",
 						username,
 						email,
@@ -112,13 +113,15 @@ export const useUserAuthStore = defineStore("userAuth", {
 							return reject(new Error(res.message));
 						}
 					);
+				}
 			});
 		},
 		login(user) {
 			return new Promise((resolve, reject) => {
 				const { email, password } = user;
 
-				ws.socket.dispatch("users.login", email, password, res => {
+				const { socket } = useWebsocketsStore();
+				socket.dispatch("users.login", email, password, res => {
 					if (res.status === "success") {
 						return lofig.get("cookie").then(cookie => {
 							const date = new Date();
@@ -156,7 +159,8 @@ export const useUserAuthStore = defineStore("userAuth", {
 		},
 		logout() {
 			return new Promise((resolve, reject) => {
-				ws.socket.dispatch("users.logout", res => {
+				const { socket } = useWebsocketsStore();
+				socket.dispatch("users.logout", res => {
 					if (res.status === "success") {
 						return resolve(
 							lofig.get("cookie").then(cookie => {
@@ -175,32 +179,29 @@ export const useUserAuthStore = defineStore("userAuth", {
 				if (typeof this.userIdMap[`Z${userId}`] !== "string") {
 					if (this.userIdRequested[`Z${userId}`] !== true) {
 						this.requestingUserId(userId);
-						ws.socket.dispatch(
-							"users.getBasicUser",
-							userId,
-							res => {
-								if (res.status === "success") {
-									const user = res.data;
+						const { socket } = useWebsocketsStore();
+						socket.dispatch("users.getBasicUser", userId, res => {
+							if (res.status === "success") {
+								const user = res.data;
 
-									this.mapUserId({
-										userId,
-										user: {
-											name: user.name,
-											username: user.username
-										}
-									});
+								this.mapUserId({
+									userId,
+									user: {
+										name: user.name,
+										username: user.username
+									}
+								});
 
-									this.pendingUserIdCallbacks[
-										`Z${userId}`
-									].forEach(cb => cb(user));
+								this.pendingUserIdCallbacks[
+									`Z${userId}`
+								].forEach(cb => cb(user));
 
-									this.clearPendingCallbacks(userId);
+								this.clearPendingCallbacks(userId);
 
-									return resolve(user);
-								}
-								return resolve(null);
+								return resolve(user);
 							}
-						);
+							return resolve(null);
+						});
 					} else {
 						this.pendingUser({
 							userId,
@@ -250,7 +251,8 @@ export const useUserAuthStore = defineStore("userAuth", {
 		},
 		updatePermissions() {
 			return new Promise(resolve => {
-				ws.socket.dispatch("utils.getPermissions", res => {
+				const { socket } = useWebsocketsStore();
+				socket.dispatch("utils.getPermissions", res => {
 					this.permissions = res.data.permissions;
 					this.gotPermissions = true;
 					resolve(this.permissions);

+ 27 - 17
frontend/src/stores/websockets.ts

@@ -1,5 +1,5 @@
 import { defineStore } from "pinia";
-import { CustomWebSocket } from "@/types/customWebSocket";
+import CustomWebSocket from "@/classes/CustomWebSocket.class";
 
 export const useWebsocketsStore = defineStore("websockets", {
 	state: () => ({
@@ -8,24 +8,34 @@ export const useWebsocketsStore = defineStore("websockets", {
 		}
 	}),
 	actions: {
-		createSocket(socket) {
-			const { listeners } = this.socket.dispatcher;
-			this.socket = socket;
+		createSocket(): Promise<CustomWebSocket> {
+			return new Promise((resolve, reject) => {
+				lofig
+					.get("backend.websocketsDomain")
+					.then(websocketsDomain => {
+						const { listeners } = this.socket.dispatcher;
 
-			// only executes if the websocket object is being replaced
-			if (listeners) {
-				// for each listener type
-				Object.keys(listeners).forEach(listenerType =>
-					// for each callback previously present for the listener type
-					listeners[listenerType].forEach(element => {
-						// add the listener back after the websocket object is reset
-						this.socket.dispatcher.addEventListener(
-							listenerType,
-							element.cb
-						);
+						this.socket = new CustomWebSocket(websocketsDomain);
+
+						// only executes if the websocket object is being replaced
+						if (listeners) {
+							// for each listener type
+							Object.keys(listeners).forEach(listenerType =>
+								// for each callback previously present for the listener type
+								listeners[listenerType].forEach(element => {
+									// add the listener back after the websocket object is reset
+									this.socket.dispatcher.addEventListener(
+										listenerType,
+										element.cb
+									);
+								})
+							);
+						}
+
+						resolve(this.socket);
 					})
-				);
-			}
+					.catch(err => reject(err));
+			});
 		}
 	},
 	getters: {

+ 0 - 7
frontend/src/types/customWebSocket.ts

@@ -1,7 +0,0 @@
-import ListenerHandler from "@/classes/ListenerHandler.class";
-// TODO: Replace
-export interface CustomWebSocket extends WebSocket {
-	dispatcher: ListenerHandler;
-	on(target, cb, options?: any): void;
-	dispatch(...args): void;
-}

+ 0 - 197
frontend/src/ws.ts

@@ -1,197 +0,0 @@
-import { useWebsocketsStore } from "@/stores/websockets";
-import { useUserAuthStore } from "@/stores/userAuth";
-import ListenerHandler from "./classes/ListenerHandler.class";
-
-const onConnect = [];
-let ready = false;
-let firstInit = true;
-
-let pendingDispatches = [];
-
-const onDisconnect = {
-	temp: [],
-	persist: []
-};
-
-// references for when a dispatch event is ready to callback from server to client
-const CB_REFS = {};
-let CB_REF = 0;
-
-const PROGRESS_CB_REFS = {};
-
-export default {
-	socket: null,
-	dispatcher: null,
-
-	onConnect(cb) {
-		if (this.socket.readyState === 1 && ready) cb();
-
-		return onConnect.push(cb);
-	},
-
-	onDisconnect(...args) {
-		if (args[0] === true) onDisconnect.persist.push(args[1]);
-		else onDisconnect.temp.push(args[0]);
-	},
-
-	clearCallbacks: () => {
-		onDisconnect.temp = [];
-	},
-
-	destroyListeners() {
-		Object.keys(CB_REFS).forEach(id => {
-			if (
-				id.indexOf("$event:") !== -1 &&
-				id.indexOf("$event:keep.") === -1
-			)
-				delete CB_REFS[id];
-		});
-
-		Object.keys(PROGRESS_CB_REFS).forEach(id => {
-			if (
-				id.indexOf("$event:") !== -1 &&
-				id.indexOf("$event:keep.") === -1
-			)
-				delete PROGRESS_CB_REFS[id];
-		});
-
-		// destroy all listeners that aren't site-wide
-		Object.keys(this.socket.dispatcher.listeners).forEach(type => {
-			if (type.indexOf("keep.") === -1 && type !== "ready")
-				delete this.socket.dispatcher.listeners[type];
-		});
-	},
-
-	destroyModalListeners(modalUuid) {
-		// destroy all listeners for a specific modal
-		Object.keys(this.socket.dispatcher.listeners).forEach(type =>
-			this.socket.dispatcher.listeners[type].forEach((element, index) => {
-				if (element.options && element.options.modalUuid === modalUuid)
-					this.socket.dispatcher.listeners[type].splice(index, 1);
-			})
-		);
-	},
-
-	init(url) {
-		const userAuthStore = useUserAuthStore();
-
-		// ensures correct context of socket object when dispatching (because socket object is recreated on reconnection)
-		const waitForConnectionToDispatch = (...args) =>
-			this.socket.dispatch(...args);
-
-		class CustomWebSocket extends WebSocket {
-			dispatcher: ListenerHandler;
-
-			constructor() {
-				super(url);
-				this.dispatcher = new ListenerHandler();
-			}
-
-			on(target, cb, options) {
-				this.dispatcher.addEventListener(
-					target,
-					event => cb(...event.detail),
-					options
-				);
-			}
-
-			dispatch(...args) {
-				if (this.readyState !== 1)
-					return pendingDispatches.push(() =>
-						waitForConnectionToDispatch(...args)
-					);
-
-				const lastArg = args[args.length - 1];
-
-				if (typeof lastArg === "function") {
-					CB_REF += 1;
-					CB_REFS[CB_REF] = lastArg;
-
-					return this.send(
-						JSON.stringify([...args.slice(0, -1), { CB_REF }])
-					);
-				}
-				if (typeof lastArg === "object") {
-					CB_REF += 1;
-					CB_REFS[CB_REF] = lastArg.cb;
-					PROGRESS_CB_REFS[CB_REF] = lastArg.onProgress;
-
-					return this.send(
-						JSON.stringify([
-							...args.slice(0, -1),
-							{ CB_REF, onProgress: true }
-						])
-					);
-				}
-
-				return this.send(JSON.stringify([...args]));
-			}
-		}
-
-		this.socket = new CustomWebSocket();
-		const { createSocket } = useWebsocketsStore();
-		createSocket(this.socket);
-
-		this.socket.onopen = () => {
-			console.log("WS: SOCKET OPENED");
-		};
-
-		this.socket.onmessage = message => {
-			const data = JSON.parse(message.data);
-			const name = data.shift(0);
-
-			if (name === "CB_REF") {
-				const CB_REF = data.shift(0);
-				CB_REFS[CB_REF](...data);
-				return delete CB_REFS[CB_REF];
-			}
-			if (name === "PROGRESS_CB_REF") {
-				const PROGRESS_CB_REF = data.shift(0);
-				PROGRESS_CB_REFS[PROGRESS_CB_REF](...data);
-			}
-
-			if (name === "ERROR") console.log("WS: SOCKET ERROR:", data[0]);
-
-			return this.socket.dispatcher.dispatchEvent(
-				new CustomEvent(name, {
-					detail: data
-				})
-			);
-		};
-
-		this.socket.onclose = () => {
-			console.log("WS: SOCKET CLOSED");
-
-			ready = false;
-
-			onDisconnect.temp.forEach(cb => cb());
-			onDisconnect.persist.forEach(cb => cb());
-
-			// try to reconnect every 1000ms, if the user isn't banned
-			if (!userAuthStore.banned) setTimeout(() => this.init(url), 1000);
-		};
-
-		this.socket.onerror = err => {
-			console.log("WS: SOCKET ERROR", err);
-		};
-
-		if (firstInit) {
-			firstInit = false;
-			this.socket.on("ready", () => {
-				console.log("WS: SOCKET READY");
-
-				onConnect.forEach(cb => cb());
-
-				ready = true;
-
-				setTimeout(() => {
-					// dispatches that were attempted while the server was offline
-					pendingDispatches.forEach(cb => cb());
-					pendingDispatches = [];
-				}, 150); // small delay between readyState being 1 and the server actually receiving dispatches
-
-				userAuthStore.updatePermissions();
-			});
-		}
-	}
-};