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.
311 lines
7.0 KiB
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;
|
|
}
|
|
|