"use strict"; export class Domain { name; id; servers; invalidServers; toString() { return `[${this.name}]`; } constructor(jsonData, domainServers) { // FIXME check values this.name = jsonData.name; this.id = jsonData.id; this.servers = []; for (const domainServer of domainServers) { this.servers.push(new DomainServer(domainServer)); } this.invalidServers = []; } static async fromName(domainName) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 2000); const response = await fetch(`https://${domainName}/.well-known/usimp.json`, { redirect: "manual", signal: controller.signal, }); clearTimeout(timer); if (!response.ok) { throw Error("Invalid response"); } const data = await response.json(); return new Domain(data.domain, data.domain_servers); } chooseDomainServer() { if (this.servers.length === 0) throw Error("No domain servers specified"); const servers = this.servers.filter(srv => !this.invalidServers.map(srv => srv.id).includes(srv.id)); if (servers.length === 0) throw Error("No domain servers reachable"); if (servers.length === 1) return servers[0]; const priority = servers.reduce((min, srv) => Math.min(min, srv.priority), Infinity); const domainServers = servers.filter(srv => srv.priority === priority); if (domainServers.length === 1) return servers[0]; const totalWeight = domainServers.reduce((total, srv) => total + srv.weight, 0); const w = Math.floor(Math.random() * totalWeight); let accumulator = 0; for (const srv of domainServers) { accumulator += srv.weight; if (w < accumulator) return srv; } throw Error("Domain server selection did not work correctly"); } } export class DomainServer { host; id; priority; weight; protocols; toString() { return `[${this.host}/${this.priority}/${this.weight}]`; } constructor(jsonData) { // FIXME check values this.host = jsonData.host; this.id = jsonData.id; this.priority = jsonData.priority; this.weight = jsonData.weight; this.protocols = jsonData.protocols; } } export class Account { } export class Member { } export class Room { } export class Event { } export class Session { domain; server; token; httpBaseUrl; websocket; numRequests; constructor(domain) { this.domain = domain; this.numRequests = 0; } async chooseDomainServer() { while (!this.server) { this.server = this.domain.chooseDomainServer(); const host = this.server.host; const protocols = this.server.protocols; /* if ("ws" in protocols) { this.websocket = new WebSocket(`ws://${host}:${protocols.ws}/_usimp/websocket`, ["usimp"]); } */ // TODO http -> https if ("http" in protocols) { this.httpBaseUrl = `http://${host}:${protocols.http}/_usimp`; try { return await this.ping(); } catch { this.domain.invalidServers.push(this.server); this.server = null; } } else { console.warn(`Domain server ${this.server} does not support 'https' transport protocol`); this.domain.invalidServers.push(this.server); this.server = null; } } } async send(endpoint, data, timeout = 2000, forceHttp = false) { this.numRequests++; if (!forceHttp && this.websocket) { this.websocket.send(JSON.stringify({ 'request_num': this.numRequests, 'data': data })); } else { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); let headers = { 'Content-Type': 'application/json', 'To-Domain': this.domain.id, }; if (this.token) { headers['Authorization'] = `usimp ${this.token}`; } const startTime = new Date(); const response = await fetch(`${this.httpBaseUrl}/${endpoint}`, { method: "POST", headers: headers, body: JSON.stringify(data), signal: controller.signal, }); const endTime = new Date(); clearTimeout(timer); const responseData = await response.json(); responseData.duration = endTime - startTime; return responseData; } } async ping() { let result = {"http": null, "ws": null}; const resHttp = await this.send("ping", {}, undefined, true); result.http = resHttp.duration; if (this.websocket) { const resWs = await this.send("ping", {}); result.ws = resWs.duration; } return result; } async authenticate(accountName, password) { const response = await this.send("authenticate", { type: "password", name: accountName, password: password, }); if (response.status === "success") { this.token = response.data.token; } return response; } async sendEvent(roomId, data) { return this.send("send_event", { room_id: roomId, data: data, }); } subscribe(func) { this.numRequests++; if (this.websocket) { // TODO } else { let headers = { 'Content-Type': 'application/json', 'To-Domain': this.domain.id, }; if (this.token) { headers['Authorization'] = `usimp ${this.token}`; } fetch(`${this.httpBaseUrl}/subscribe`, { method: "POST", headers: headers, body: JSON.stringify({}), }).then(response => { return response.json(); }).then(response => { if (response.status === "success") { this.subscribe(func); func(response); } else { setTimeout(() => { this.subscribe(func); }, 1000); } }).catch(() => { setTimeout(() => { this.subscribe(func); }, 1000); }); } } }