瀏覽代碼

feat(socket.io -> WS): client-side tries to reconnect every 1000ms if loses connection

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 年之前
父節點
當前提交
4787d6aa37

+ 2 - 5
frontend/src/App.vue

@@ -48,7 +48,6 @@ export default {
 	}),
 	watch: {
 		socketConnected(connected) {
-			console.log(connected);
 			if (!connected)
 				new Toast({
 					content: "Could not connect to the server.",
@@ -109,12 +108,9 @@ export default {
 		}
 
 		io.onConnect(true, () => {
-			console.log("APP.VUE", "onConnect(true, () => {})");
 			this.socketConnected = true;
 		});
-		io.onConnectError(true, () => {
-			this.socketConnected = false;
-		});
+
 		io.onDisconnect(true, () => {
 			this.socketConnected = false;
 		});
@@ -139,6 +135,7 @@ export default {
 				new Toast({ content: msg, timeout: 20000 });
 			}
 		});
+
 		io.getSocket(true, socket => {
 			this.socket = socket;
 

+ 61 - 31
frontend/src/io.js

@@ -1,5 +1,8 @@
 import Toast from "toasters";
 
+// eslint-disable-next-line import/no-cycle
+import store from "./store";
+
 const callbacks = {
 	general: {
 		temp: [],
@@ -12,10 +15,6 @@ const callbacks = {
 	onDisconnect: {
 		temp: [],
 		persist: []
-	},
-	onConnectError: {
-		temp: [],
-		persist: []
 	}
 };
 
@@ -23,23 +22,20 @@ const callbackss = {};
 let callbackRef = 0;
 
 export default {
-	ready: false,
 	socket: null,
 	dispatcher: null,
 
 	getSocket(...args) {
 		if (args[0] === true) {
-			if (this.ready) {
+			if (this.socket && this.socket.readyState === 1) {
 				args[1](this.socket);
 			} else callbacks.general.persist.push(args[1]);
-		} else if (this.ready) {
+		} else if (this.socket && this.socket.readyState === 1) {
 			args[0](this.socket);
 		} else callbacks.general.temp.push(args[0]);
 	},
 
 	onConnect(...args) {
-		console.log("on connect io.js", args);
-
 		if (args[0] === true) callbacks.onConnect.persist.push(args[1]);
 		else callbacks.onConnect.temp.push(args[0]);
 	},
@@ -49,11 +45,6 @@ export default {
 		else callbacks.onDisconnect.temp.push(args[0]);
 	},
 
-	onConnectError(...args) {
-		if (args[0] === true) callbacks.onDisconnect.persist.push(args[1]);
-		else callbacks.onConnectError.temp.push(args[0]);
-	},
-
 	clear: () => {
 		Object.keys(callbacks).forEach(type => {
 			callbacks[type].temp = [];
@@ -70,11 +61,44 @@ export default {
 		});
 	},
 
-	init(url) {
-		class CustomWebSocket extends WebSocket {
+	init() {
+		class ListenerHandler extends EventTarget {
 			constructor() {
+				super();
+				this.listeners = {};
+			}
+
+			addEventListener(type, cb) {
+				if (!(type in this.listeners)) this.listeners[type] = []; // add the listener type to listeners object
+				this.listeners[type].push(cb); // push the callback
+			}
+
+			// eslint-disable-next-line consistent-return
+			removeEventListener(type, cb) {
+				if (!(type in this.listeners)) return true; // event type doesn't exist
+
+				const stack = this.listeners[type];
+
+				for (let i = 0, l = stack.length; i < l; i += 1)
+					if (stack[i] === cb) stack.splice(i, 1);
+			}
+
+			dispatchEvent(event) {
+				if (!(event.type in this.listeners)) return true; // event type doesn't exist
+
+				const stack = this.listeners[event.type].slice();
+
+				for (let i = 0, l = stack.length; i < l; i += 1)
+					stack[i].call(this, event);
+
+				return !event.defaultPrevented;
+			}
+		}
+
+		class CustomWebSocket extends WebSocket {
+			constructor(url) {
 				super(url);
-				this.dispatcher = new EventTarget();
+				this.dispatcher = new ListenerHandler();
 			}
 
 			on(target, cb) {
@@ -86,27 +110,32 @@ export default {
 			dispatch(...args) {
 				callbackRef += 1;
 
+				if (this.readyState !== 1)
+					return new Toast({
+						content: "Cannot perform this action at this time.",
+						timeout: 8000
+					});
+
 				const cb = args[args.length - 1];
 				if (typeof cb === "function") callbackss[callbackRef] = cb;
 
-				this.send(
+				return this.send(
 					JSON.stringify([...args.slice(0, -1), { callbackRef }])
 				);
 			}
 		}
 
-		this.socket = window.socket = new CustomWebSocket(url);
+		this.socket = new CustomWebSocket("ws://localhost:8080/ws");
+		store.dispatch("websockets/createSocket", this.socket);
 
 		this.socket.onopen = () => {
 			callbacks.onConnect.temp.forEach(cb => cb());
 			callbacks.onConnect.persist.forEach(cb => cb());
 
-			this.ready = true;
+			console.log("IO: SOCKET CONNECTED");
 
-			callbacks.general.temp.forEach(callback => callback(this.socket));
-			callbacks.general.persist.forEach(callback =>
-				callback(this.socket)
-			);
+			callbacks.general.temp.forEach(cb => cb(this.socket));
+			callbacks.general.persist.forEach(cb => cb(this.socket));
 
 			callbacks.general.temp = [];
 			callbacks.general.persist = [];
@@ -131,20 +160,21 @@ export default {
 
 		this.socket.onclose = () => {
 			console.log("IO: SOCKET DISCONNECTED");
+
 			callbacks.onDisconnect.temp.forEach(cb => cb());
 			callbacks.onDisconnect.persist.forEach(cb => cb());
-		};
 
-		// this.socket.on("connect_error", () => {
-		// 	console.log("IO: SOCKET CONNECT ERROR");
-		// 	callbacks.onConnectError.temp.forEach(cb => cb());
-		// 	callbacks.onConnectError.persist.forEach(cb => cb());
-		// });
+			// try to reconnect every 1000ms
+			setTimeout(() => {
+				this.init();
+			}, 1000);
+		};
 
 		this.socket.onerror = err => {
 			console.log("IO: SOCKET ERROR", err);
+
 			new Toast({
-				content: err,
+				content: "Cannot perform this action at this time.",
 				timeout: 8000
 			});
 		};

+ 1 - 1
frontend/src/main.js

@@ -149,7 +149,7 @@ lofig.fetchConfig().then(config => {
 	}
 
 	// const { serverDomain } = config;
-	io.init("ws://localhost:8080/ws");
+	io.init({ url: "ws://localhost:8080/ws" });
 
 	io.getSocket(socket => {
 		socket.on("ready", (loggedIn, role, username, userId) => {

+ 129 - 131
frontend/src/pages/Home.vue

@@ -437,7 +437,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from "vuex";
+import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
 
 import MainHeader from "../components/layout/MainHeader.vue";
@@ -470,6 +470,9 @@ export default {
 			userId: state => state.user.auth.userId,
 			modals: state => state.modalVisibility.modals.home
 		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		}),
 		filteredStations() {
 			const privacyOrder = ["public", "unlisted", "private"];
 			return this.stations
@@ -498,161 +501,156 @@ export default {
 	async mounted() {
 		this.siteName = await lofig.get("siteSettings.siteName");
 
-		io.getSocket(socket => {
-			this.socket = socket;
-
-			if (this.socket.readyState === 1) this.init();
-			io.onConnect(() => this.init());
-
-			this.socket.on("event:stations.created", res => {
-				const station = res;
-				if (
-					this.stations.find(_station => _station._id === station._id)
-				) {
-					this.stations.forEach(s => {
-						const _station = s;
-						if (_station._id === station._id) {
-							_station.privacy = station.privacy;
-						}
-					});
-				} else {
-					if (!station.currentSong)
-						station.currentSong = {
-							thumbnail: "/assets/notes-transparent.png"
-						};
-					if (station.currentSong && !station.currentSong.thumbnail)
-						station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
-					this.stations.push(station);
-				}
-			});
+		// io.getSocket(socket => {
+		// 	this.socket = socket;
 
-			this.socket.on("event:station.removed", response => {
-				const { stationId } = response;
-				const station = this.stations.find(
-					station => station._id === stationId
-				);
-				if (station) {
-					const stationIndex = this.stations.indexOf(station);
-					this.stations.splice(stationIndex, 1);
-				}
-			});
+		if (this.socket.readyState === 1) this.init();
+		io.onConnect(() => this.init());
 
-			this.socket.on(
-				"event:userCount.updated",
-				(stationId, userCount) => {
-					this.stations.forEach(s => {
-						const station = s;
-						if (station._id === stationId) {
-							station.userCount = userCount;
-						}
-					});
-				}
-			);
-
-			this.socket.on("event:station.updatePrivacy", response => {
-				const { stationId, privacy } = response;
+		this.socket.on("event:stations.created", res => {
+			const station = res;
+			if (this.stations.find(_station => _station._id === station._id)) {
 				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.privacy = privacy;
+					const _station = s;
+					if (_station._id === station._id) {
+						_station.privacy = station.privacy;
 					}
 				});
+			} else {
+				if (!station.currentSong)
+					station.currentSong = {
+						thumbnail: "/assets/notes-transparent.png"
+					};
+				if (station.currentSong && !station.currentSong.thumbnail)
+					station.currentSong.ytThumbnail = `https://img.youtube.com/vi/${station.currentSong.songId}/mqdefault.jpg`;
+				this.stations.push(station);
+			}
+		});
+
+		this.socket.on("event:station.removed", response => {
+			const { stationId } = response;
+			const station = this.stations.find(
+				station => station._id === stationId
+			);
+			if (station) {
+				const stationIndex = this.stations.indexOf(station);
+				this.stations.splice(stationIndex, 1);
+			}
+		});
+
+		this.socket.on("event:userCount.updated", (stationId, userCount) => {
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.userCount = userCount;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateName", response => {
-				const { stationId, name } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.name = name;
-					}
-				});
+		this.socket.on("event:station.updatePrivacy", response => {
+			const { stationId, privacy } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.privacy = privacy;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateDisplayName", response => {
-				const { stationId, displayName } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.displayName = displayName;
-					}
-				});
+		this.socket.on("event:station.updateName", response => {
+			const { stationId, name } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.name = name;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateDescription", response => {
-				const { stationId, description } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.description = description;
-					}
-				});
+		this.socket.on("event:station.updateDisplayName", response => {
+			const { stationId, displayName } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.displayName = displayName;
+				}
+			});
+		});
+
+		this.socket.on("event:station.updateDescription", response => {
+			const { stationId, description } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.description = description;
+				}
 			});
+		});
 
-			this.socket.on("event:station.updateTheme", response => {
-				const { stationId, theme } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.theme = theme;
-					}
-				});
+		this.socket.on("event:station.updateTheme", response => {
+			const { stationId, theme } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.theme = theme;
+				}
 			});
+		});
 
-			this.socket.on("event:station.nextSong", (stationId, song) => {
-				let newSong = song;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						if (!newSong)
-							newSong = {
-								thumbnail: "/assets/notes-transparent.png"
-							};
-						if (newSong && !newSong.thumbnail)
-							newSong.ytThumbnail = `https://img.youtube.com/vi/${newSong.songId}/mqdefault.jpg`;
-						station.currentSong = newSong;
-					}
-				});
+		this.socket.on("event:station.nextSong", (stationId, song) => {
+			let newSong = song;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					if (!newSong)
+						newSong = {
+							thumbnail: "/assets/notes-transparent.png"
+						};
+					if (newSong && !newSong.thumbnail)
+						newSong.ytThumbnail = `https://img.youtube.com/vi/${newSong.songId}/mqdefault.jpg`;
+					station.currentSong = newSong;
+				}
 			});
+		});
 
-			this.socket.on("event:station.pause", response => {
-				const { stationId } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.paused = true;
-					}
-				});
+		this.socket.on("event:station.pause", response => {
+			const { stationId } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.paused = true;
+				}
 			});
+		});
 
-			this.socket.on("event:station.resume", response => {
-				const { stationId } = response;
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.paused = false;
-					}
-				});
+		this.socket.on("event:station.resume", response => {
+			const { stationId } = response;
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.paused = false;
+				}
 			});
+		});
 
-			this.socket.on("event:user.favoritedStation", stationId => {
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.isFavorited = true;
-					}
-				});
+		this.socket.on("event:user.favoritedStation", stationId => {
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.isFavorited = true;
+				}
 			});
+		});
 
-			this.socket.on("event:user.unfavoritedStation", stationId => {
-				this.stations.forEach(s => {
-					const station = s;
-					if (station._id === stationId) {
-						station.isFavorited = false;
-					}
-				});
+		this.socket.on("event:user.unfavoritedStation", stationId => {
+			this.stations.forEach(s => {
+				const station = s;
+				if (station._id === stationId) {
+					station.isFavorited = false;
+				}
 			});
 		});
+		// });
 	},
 	methods: {
 		init() {

+ 4 - 1
frontend/src/pages/Station/index.vue

@@ -599,7 +599,10 @@ export default {
 			this.socket = socket;
 
 			if (this.socket.readyState === 1) this.join();
-			io.onConnect(this.join);
+			io.onConnect(() => {
+				console.log("station page connect", this.socket.readyState);
+				this.join();
+			});
 
 			this.socket.dispatch(
 				"stations.existsByName",

+ 4 - 0
frontend/src/store/index.js

@@ -1,6 +1,9 @@
+/* eslint-disable import/no-cycle */
 import Vue from "vue";
 import Vuex from "vuex";
 
+import websockets from "./modules/websockets";
+
 import user from "./modules/user";
 import settings from "./modules/settings";
 import modalVisibility from "./modules/modalVisibility";
@@ -19,6 +22,7 @@ Vue.use(Vuex);
 
 export default new Vuex.Store({
 	modules: {
+		websockets,
 		user,
 		settings,
 		station,

+ 42 - 0
frontend/src/store/modules/websockets.js

@@ -0,0 +1,42 @@
+/* eslint no-param-reassign: 0 */
+
+const state = {
+	socket: {
+		dispatcher: {}
+	}
+};
+
+const getters = {
+	getSocket: state => state.socket
+};
+
+const actions = {
+	createSocket: ({ commit }, socket) => commit("createSocket", socket)
+};
+
+const mutations = {
+	createSocket(state, socket) {
+		const { listeners } = state.socket.dispatcher;
+		state.socket = socket;
+
+		// 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(cb =>
+					// add the listener back after the websocket object is reset
+					state.socket.dispatcher.addEventListener(listenerType, cb)
+				)
+			);
+		}
+	}
+};
+
+export default {
+	namespaced: true,
+	state,
+	getters,
+	actions,
+	mutations
+};