Enforce type cheking for server communication
This commit is contained in:
236
src/usimp.ts
236
src/usimp.ts
@ -1,46 +1,135 @@
|
||||
"use strict";
|
||||
|
||||
interface DomainJson {
|
||||
const DNS_RE = /^([a-z0-9_-]+\.)+[a-z]{2,}$/i;
|
||||
const UUID_RE = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
||||
|
||||
type DomainJson = {
|
||||
name: string,
|
||||
id: string,
|
||||
}
|
||||
|
||||
interface DomainServerJson {
|
||||
function isDomainJson(json: any): json is DomainJson {
|
||||
return (
|
||||
json !== null && typeof json === 'object' && json.constructor === Object &&
|
||||
typeof json.name === 'string' && typeof json.id === 'string' &&
|
||||
DNS_RE.test(json.name) && UUID_RE.test(json.id)
|
||||
);
|
||||
}
|
||||
|
||||
type DomainServerJson = {
|
||||
host: string,
|
||||
id: string,
|
||||
priority: number,
|
||||
weight: number,
|
||||
protocols: {
|
||||
https: number | undefined,
|
||||
wss: number | undefined,
|
||||
http: number | undefined,
|
||||
ws: number | undefined,
|
||||
udp: number | undefined
|
||||
https?: number | null,
|
||||
wss?: number | null,
|
||||
http?: number | null,
|
||||
ws?: number | null,
|
||||
udp?: number | null
|
||||
}
|
||||
}
|
||||
|
||||
interface OutputEnvelopeJson {
|
||||
function isDomainServerJson(json: any): json is DomainServerJson {
|
||||
if (
|
||||
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||
typeof json.host !== 'string' || typeof json.id !== 'string' || typeof json.priority !== 'number' ||
|
||||
typeof json.weight !== 'number' ||
|
||||
json.protocols === null || typeof json.protocols !== 'object' || json.protocols.constructor !== Object ||
|
||||
!DNS_RE.test(json.host) || !UUID_RE.test(json.id) || json.priority < 0 || json.weight < 0
|
||||
) return false;
|
||||
|
||||
|
||||
return (
|
||||
(json.protocols.https === undefined || json.protocols.https === null || (
|
||||
typeof json.protocols.https === 'number' && json.protocols.https > 0 && json.protocols.https <= 65535
|
||||
)) && (json.protocols.wss === undefined || json.protocols.wss === null || (
|
||||
typeof json.protocols.wss === 'number' && json.protocols.wss > 0 && json.protocols.wss <= 65535
|
||||
)) && (json.protocols.http === undefined || json.protocols.http === null || (
|
||||
typeof json.protocols.http === 'number' && json.protocols.http > 0 && json.protocols.http <= 65535
|
||||
)) && (json.protocols.ws === undefined || json.protocols.ws === null || (
|
||||
typeof json.protocols.ws === 'number' && json.protocols.ws > 0 && json.protocols.ws <= 65535
|
||||
)) && (json.protocols.udp === undefined || json.protocols.udp === null || (
|
||||
typeof json.protocols.udp === 'number' && json.protocols.udp > 0 && json.protocols.udp <= 65535
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
type WellKnownJson = {
|
||||
domain: DomainJson,
|
||||
domain_servers: DomainServerJson[],
|
||||
}
|
||||
|
||||
function isWellKnownJson(json: any): json is WellKnownJson {
|
||||
if (
|
||||
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||
!json.hasOwnProperty('domain') || !json.hasOwnProperty('domain_servers') ||
|
||||
json.domain_servers === null || typeof json.domain_servers !== 'object' ||
|
||||
json.domain_servers.constructor !== Array
|
||||
) return false;
|
||||
|
||||
return isDomainJson(json.domain) && json.domain_servers.every((item: unknown) => isDomainServerJson(item));
|
||||
}
|
||||
|
||||
type OutputEnvelopeJson<T> = {
|
||||
status: string,
|
||||
request_nr: number,
|
||||
data: any,
|
||||
action: string | null | undefined,
|
||||
error: {
|
||||
data: T,
|
||||
action?: string | null,
|
||||
error?: {
|
||||
code: string,
|
||||
message: string | null,
|
||||
description: string | null,
|
||||
} | null,
|
||||
}
|
||||
|
||||
interface EventJson {
|
||||
data: {
|
||||
message: string,
|
||||
function isOutputEnvelopeJson<T>(json: any): json is OutputEnvelopeJson<T> {
|
||||
if (
|
||||
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||
typeof json.status !== 'string' || typeof json.request_nr !== 'number' || json.request_nr < 0 ||
|
||||
(typeof json.action !== 'string' && json.action !== null && json.action === undefined) ||
|
||||
json.data === null || typeof json.data !== 'object' || json.data.constructor !== Object
|
||||
) return false;
|
||||
|
||||
if (json.error !== null && json.error !== undefined) {
|
||||
if (
|
||||
typeof json.error !== 'object' || json.error.constructor !== Object ||
|
||||
typeof json.error.code !== 'string' ||
|
||||
(typeof json.error.message !== 'string' && json.error.message !== null) ||
|
||||
(typeof json.error.description !== 'string' && json.error.description !== null)
|
||||
) return false;
|
||||
}
|
||||
id: string,
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
interface WellKnownJson {
|
||||
domain: DomainJson,
|
||||
domain_servers: DomainServerJson[]
|
||||
type EventPushEnvelopeJson = {
|
||||
events: EventJson[],
|
||||
}
|
||||
|
||||
function isEventPushEnvelopeJson(json: any): json is EventPushEnvelopeJson {
|
||||
if (
|
||||
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||
json.events === null || typeof json.events !== 'object' || json.events.constructor !== Array
|
||||
) return false;
|
||||
|
||||
return json.events.every((item: unknown) => isEventJson(item));
|
||||
}
|
||||
|
||||
type EventJson = {
|
||||
id: string,
|
||||
data: {
|
||||
message: string,
|
||||
},
|
||||
}
|
||||
|
||||
function isEventJson(json: any): json is EventJson {
|
||||
return !(
|
||||
json === null || typeof json !== 'object' || json.constructor !== Object ||
|
||||
typeof json.id !== 'string' || !UUID_RE.test(json.id) ||
|
||||
json.data === null || typeof json.data !== 'object' || json.data.constructor !== Object ||
|
||||
typeof json.data.message !== 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export class Domain {
|
||||
@ -53,10 +142,9 @@ export class Domain {
|
||||
return `[${this.name}]`;
|
||||
}
|
||||
|
||||
constructor(jsonData: DomainJson, domainServers: DomainServerJson[]) {
|
||||
// FIXME check values
|
||||
this.name = jsonData.name;
|
||||
this.id = jsonData.id;
|
||||
constructor(json: DomainJson, domainServers: DomainServerJson[]) {
|
||||
this.name = json.name;
|
||||
this.id = json.id;
|
||||
this.servers = [];
|
||||
for (const domainServer of domainServers) {
|
||||
this.servers.push(new DomainServer(domainServer));
|
||||
@ -78,8 +166,12 @@ export class Domain {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
|
||||
const data: WellKnownJson = await response.json();
|
||||
const data = await response.json();
|
||||
if (isWellKnownJson(data)) {
|
||||
return new Domain(data.domain, data.domain_servers);
|
||||
} else {
|
||||
throw Error("Invalid contents in usimp.json");
|
||||
}
|
||||
}
|
||||
|
||||
chooseDomainServer(): DomainServer {
|
||||
@ -112,23 +204,29 @@ export class DomainServer {
|
||||
priority: number;
|
||||
weight: number;
|
||||
protocols: {
|
||||
https: number | undefined,
|
||||
wss: number | undefined,
|
||||
http: number | undefined,
|
||||
ws: number | undefined,
|
||||
udp: number | undefined
|
||||
https: number | null,
|
||||
wss: number | null,
|
||||
http: number | null,
|
||||
ws: number | null,
|
||||
udp: number | null
|
||||
};
|
||||
|
||||
toString() {
|
||||
return `[${this.host}/${this.priority}/${this.weight}]`;
|
||||
}
|
||||
|
||||
constructor(jsonData: DomainServerJson) {
|
||||
this.host = jsonData.host;
|
||||
this.id = jsonData.id;
|
||||
this.priority = jsonData.priority;
|
||||
this.weight = jsonData.weight;
|
||||
this.protocols = jsonData.protocols;
|
||||
constructor(json: DomainServerJson) {
|
||||
this.host = json.host;
|
||||
this.id = json.id;
|
||||
this.priority = json.priority;
|
||||
this.weight = json.weight;
|
||||
this.protocols = {
|
||||
https: json.protocols.https || null,
|
||||
wss: json.protocols.wss || null,
|
||||
http: json.protocols.http || null,
|
||||
ws: json.protocols.ws || null,
|
||||
udp: json.protocols.udp || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,9 +243,51 @@ export class Room {
|
||||
}
|
||||
|
||||
export class Event {
|
||||
id: string;
|
||||
data: {
|
||||
message: string;
|
||||
};
|
||||
|
||||
constructor(json: EventJson) {
|
||||
this.data = {
|
||||
message: json.data.message,
|
||||
};
|
||||
this.id = json.id;
|
||||
}
|
||||
}
|
||||
|
||||
type EventHandler = (event: Event) => void;
|
||||
|
||||
export class OutputEnvelope<T> {
|
||||
status: string;
|
||||
requestNr: number;
|
||||
data: T;
|
||||
action: string | null;
|
||||
error: {
|
||||
code: string,
|
||||
message: string | null,
|
||||
description: string | null,
|
||||
} | null;
|
||||
|
||||
constructor(json: OutputEnvelopeJson<T>) {
|
||||
this.status = json.status;
|
||||
this.requestNr = json.request_nr;
|
||||
this.data = json.data;
|
||||
this.action = json.action || null;
|
||||
if (json.error) {
|
||||
this.error = {
|
||||
code: json.error.code,
|
||||
message: json.error.message,
|
||||
description: json.error.description,
|
||||
};
|
||||
} else {
|
||||
this.error = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OutputEnvelopeHandler<T> = (envelope: OutputEnvelope<T>) => void;
|
||||
|
||||
export class Session {
|
||||
domain: Domain;
|
||||
server: DomainServer | null;
|
||||
@ -158,12 +298,12 @@ export class Session {
|
||||
numRequests: number;
|
||||
requestNumDiscriminator: number;
|
||||
subscriptions: {
|
||||
callback: (a: EventJson) => void,
|
||||
callback: EventHandler,
|
||||
requestNr: number | undefined,
|
||||
abortController: AbortController | undefined,
|
||||
}[];
|
||||
subscriptionEndpoints: {
|
||||
callback: (a: EventJson) => void,
|
||||
callback: EventHandler,
|
||||
}[];
|
||||
|
||||
constructor(domain: Domain) {
|
||||
@ -303,11 +443,11 @@ export class Session {
|
||||
|
||||
async send(
|
||||
endpoint: string,
|
||||
data: any,
|
||||
data: Object,
|
||||
timeout: number = 2000,
|
||||
forceHttp: boolean = false,
|
||||
abortController: AbortController | undefined = undefined,
|
||||
cb: ((a: OutputEnvelopeJson) => void) | undefined = undefined,
|
||||
cb: OutputEnvelopeHandler<unknown> | undefined = undefined,
|
||||
) {
|
||||
this.numRequests++;
|
||||
while (true) {
|
||||
@ -324,8 +464,12 @@ export class Session {
|
||||
} else {
|
||||
this.websocket.addEventListener("message", (msg) => {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data['request_nr'] === req_nr) {
|
||||
cb(data);
|
||||
if (data !== null && data.constructor === Object && data['request_nr'] === req_nr) {
|
||||
if (isOutputEnvelopeJson<unknown>(data)) {
|
||||
cb(new OutputEnvelope<unknown>(data));
|
||||
} else {
|
||||
throw new Error("Invalid server response");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -415,7 +559,7 @@ export class Session {
|
||||
return response;
|
||||
}
|
||||
|
||||
async newEvent(roomId: string, data: any, id: string | undefined = undefined) {
|
||||
async newEvent(roomId: string, data: Object, id: string | undefined = undefined) {
|
||||
return this.send("new_event", {
|
||||
room_id: roomId,
|
||||
events: [{
|
||||
@ -425,17 +569,21 @@ export class Session {
|
||||
});
|
||||
}
|
||||
|
||||
async subscribe(cb: (a: EventJson) => void) {
|
||||
async subscribe(cb: EventHandler) {
|
||||
this.subscriptionEndpoints.push({callback: cb});
|
||||
await this._subscribe(cb);
|
||||
}
|
||||
|
||||
private async _subscribe(cb: (a: EventJson) => void) {
|
||||
private async _subscribe(cb: EventHandler) {
|
||||
if (this.websocket !== null) {
|
||||
const subscription = await this.send('subscribe', {}, 60_000, false, undefined, (res) => {
|
||||
if (res.action === 'push') {
|
||||
if (isEventPushEnvelopeJson(res.data)) {
|
||||
for (const event of res.data.events) {
|
||||
cb(event);
|
||||
cb(new Event(event));
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid server response");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user