Kopie van https://gitlab.com/studieverenigingvia/ict/centurion met een paar aanpassingen
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
centurion/frontend/src/lib/Connection.ts

311 lines
7.0 KiB

import io, { Socket } from "socket.io-client";
import { Config, Room, RoomOptions, Timeline } from "../types/types";
import { Sub, useSub } from "../util/sub";
class Connection {
url = "/";
socket: Socket;
isConnected = new Sub<boolean>();
config = new Sub<Config | null>();
room = new Sub<Room | null>();
timeline = new Sub<Timeline | null>();
timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000];
timeSyncs: { [requestId: number]: TimeSyncRequest } = {};
timeSyncTimeoutIds: number[] = [];
timeSyncTooOld = 120000;
roomTime = new Sub<number>();
constructor() {
this.isConnected.set(false);
this.socket = io(this.url, {
autoConnect: false,
transports: ["websocket"],
});
this.setupSocketListeners();
this.connect();
this.roomTime.set(0);
}
connect() {
this.socket.connect();
}
setupSocketListeners() {
this.socket.on("connect", async () => {
this.isConnected.set(true);
await this.onConnect();
});
this.socket.on("disconnect", () => {
this.isConnected.set(false);
this.onDisconnect();
});
this.socket.on("config", (data: any) => {
this.config.set(data.config);
});
this.socket.on("time_sync", (data: any) => {
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime);
});
this.socket.on("room", (data: any) => {
if (data.room) {
this.setQueryLobbyId(data.room.id);
}
this.room.set(data.room);
});
this.socket.on("timeline", (data: any) => {
if (data.timeline) {
this.timeline.set(new Timeline(data.timeline));
} else {
this.timeline.set(null);
}
});
}
async onConnect() {
this.startTimeSync();
const lobbyId = this.getQueryLobbyId();
if (lobbyId) {
const exists = await this.requestJoin(lobbyId);
if (!exists) {
this.setQueryLobbyId();
this.requestJoinRandom();
}
} else {
this.requestJoinRandom();
}
}
onDisconnect() {
this.stopTimeSync();
}
private getQueryLobbyId(): number | null {
const query = new URLSearchParams(window.location.search);
const lobby = query.get("lobby");
if (!lobby) {
return null;
}
const lobbyId = Number.parseInt(query.get("lobby")!.toString());
if (!Number.isSafeInteger(lobbyId) || lobbyId < 1) {
return null;
}
return lobbyId;
}
private setQueryLobbyId(lobbyId?: number) {
const newUrl = new URL(window.location.href);
if (lobbyId) {
newUrl.searchParams.set("lobby", String(lobbyId));
} else {
newUrl.searchParams.delete("lobby");
}
window.history.pushState({}, "", newUrl.toString());
}
setRoomOptions(roomOptions: RoomOptions) {
this.socket.emit("room_options", roomOptions);
}
requestStart() {
this.socket.emit("request_start");
}
submitTickerMessage(message: string) {
return new Promise<void>((resolve, reject) => {
this.socket.emit(
"submit_ticker_message",
message,
(_: null, err?: string) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
async requestJoin(roomId: number): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
this.socket.emit(
"request_join",
roomId,
(err?: string, didJoinRoom?: boolean) => {
if (err) {
return reject(err);
}
return resolve(!!didJoinRoom);
}
);
});
}
requestSetReady() {
this.socket.emit("request_set_ready");
}
requestJoinRandom() {
this.socket.emit("request_join_random");
}
startTimeSync() {
for (let i = 0; i < this.timeSyncIntervals.length; i++) {
const timeoutId = setTimeout(() => {
// Only reschedule the last sync interval (i.e. every 30 seconds)
const shouldReschedule = i === this.timeSyncIntervals.length - 1;
this.sendTimeSync(shouldReschedule);
}, this.timeSyncIntervals[i]);
this.timeSyncTimeoutIds.push(timeoutId);
}
}
stopTimeSync() {
for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) {
clearTimeout(this.timeSyncTimeoutIds[i]);
}
}
sendTimeSync(alsoSchedule: boolean) {
const sync = new TimeSyncRequest();
this.socket.emit("time_sync", sync.requestId, Date.now());
this.timeSyncs[sync.requestId] = sync;
if (alsoSchedule) {
setTimeout(() => {
this.sendTimeSync(true);
}, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]);
}
}
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) {
const syncReq = this.timeSyncs[requestId];
if (!syncReq) {
return;
}
syncReq.response(clientDiff, serverTime);
for (const i in this.timeSyncs) {
if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) {
delete this.timeSyncs[i];
break;
}
}
this.roomTime.set(calculateRoomTime());
}
serverTime(): number {
return Date.now() + this.serverTimeOffset();
}
serverTimeOffset(): number {
let num = 0;
let sum = 0;
for (const i in this.timeSyncs) {
const sync = this.timeSyncs[i];
if (!sync.ready) continue;
sum += sync.offset;
num += 1;
}
if (num === 0) {
return 0;
}
return Math.round(sum / num);
}
}
let _timeSyncId = 0;
class TimeSyncRequest {
requestId: number;
start: number;
offset = 0;
ready = false;
constructor() {
this.requestId = _timeSyncId++;
this.start = Date.now();
}
response(clientDiff: number, serverTime: number) {
this.ready = true;
const now = Date.now();
const lag = now - this.start;
this.offset = serverTime - now + lag / 2;
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
// lag, 'diff:', serverTime - now, 'offset:', this.offset);
}
}
const connection: Connection = new Connection();
export default connection;
export function useRoom(): Room | null {
return useSub(connection.room);
}
export function useConfig(): Config | null {
return useSub(connection.config);
}
export function useRoomRunningAndReadyChanged(): Room | null {
return useSub(connection.room, (v) => [v, v?.running, v?.readyToParticipate]);
}
export function useTimeline(): Timeline | null {
return useSub(connection.timeline);
}
export function useTimelineSongFileChanged(): Timeline | null {
return useSub(connection.timeline, (v) => [v?.songFile]);
}
export function useRoomTime(): number {
return useSub(connection.roomTime) || 0;
}
/**
* Calculates the current room time, adjusted for any possible server time
* offset and lag.
*/
export function calculateRoomTime(): number {
const room = connection.room.get();
if (!room || typeof room.startTime === "undefined") {
return 0;
}
return (connection.serverTime() - room.startTime) * room.speedFactor;
}
export function useIsConnected(): boolean {
return useSub(connection.isConnected) || false;
}