TeaWeb/shared/js/connection/ServerFeatures.ts

171 lines
6.6 KiB
TypeScript

import {ConnectionEvents, ConnectionHandler, ConnectionState} from "../ConnectionHandler";
import {Registry} from "../events";
import {CommandResult} from "../connection/ServerConnectionDeclaration";
import {ErrorCode} from "../connection/ErrorCode";
import {LogCategory, logDebug, logTrace, logWarn} from "../log";
import {ExplicitCommandHandler} from "../connection/AbstractCommandHandler";
import { tr } from "tc-shared/i18n/localize";
export type ServerFeatureSupport = "unsupported" | "supported" | "experimental" | "deprecated";
export enum ServerFeature {
ERROR_BULKS = "error-bulks", /* Current version is 1 */
ADVANCED_CHANNEL_CHAT = "advanced-channel-chat", /* Current version is 1 */
LOG_QUERY = "log-query", /* Current version is 1 */
WHISPER_ECHO = "whisper-echo", /* Current version is 1 */
VIDEO = "video",
SIDEBAR_MODE = "sidebar-mode"
}
export interface ServerFeatureEvents {
notify_state_changed: {
feature: ServerFeature,
version?: number,
support: ServerFeatureSupport
}
}
export class ServerFeatures {
readonly events: Registry<ServerFeatureEvents>;
private readonly connection: ConnectionHandler;
private readonly explicitCommandHandler: ExplicitCommandHandler;
private readonly stateChangeListener: () => void;
private featureAwait: Promise<boolean>;
private featureAwaitCallback: (success: boolean) => void;
private featuresSet = false;
private featureStates: {[key: string]: { version?: number, support: ServerFeatureSupport }} = {};
constructor(connection: ConnectionHandler) {
this.events = new Registry<ServerFeatureEvents>();
this.connection = connection;
this.connection.getServerConnection().command_handler_boss().register_explicit_handler("notifyfeaturesupport", this.explicitCommandHandler = command => {
for(const set of command.arguments) {
let support: ServerFeatureSupport;
switch (parseInt(set["support"])) {
case 0:
support = "unsupported";
break;
case 1:
support = "supported";
break;
case 2:
support = "experimental";
break;
case 3:
support = "deprecated";
break;
default:
logWarn(LogCategory.SERVER, tr("Received feature %s with unknown support state: %s"), set["name"], set["support"])
}
this.setFeatureSupport(set["name"], support, parseInt(set["version"]));
}
});
this.stateChangeListener = this.connection.events().on("notify_connection_state_changed", event => {
if(event.newState === ConnectionState.CONNECTED) {
this.connection.getServerConnection().send_command("listfeaturesupport").catch(error => {
this.disableAllFeatures();
if(error instanceof CommandResult) {
if(error.id === ErrorCode.COMMAND_NOT_FOUND) {
logDebug(LogCategory.SERVER, tr("Target server does not support the feature list command. Disabling all features."));
return;
}
}
logWarn(LogCategory.SERVER, tr("Failed to query server features: %o"), error);
}).then(() => {
this.featuresSet = true;
if(this.featureAwaitCallback) {
this.featureAwaitCallback(true);
}
});
} else if(event.newState === ConnectionState.DISCONNECTING || event.newState === ConnectionState.UNCONNECTED) {
this.disableAllFeatures();
this.featureAwait = undefined;
this.featureAwaitCallback = undefined;
this.featuresSet = false;
}
});
}
destroy() {
this.stateChangeListener();
this.connection.getServerConnection()?.command_handler_boss()?.unregister_explicit_handler("notifyfeaturesupport", this.explicitCommandHandler);
if(this.featureAwaitCallback) {
this.featureAwaitCallback(false);
}
this.events.destroy();
}
supportsFeature(feature: ServerFeature, version?: number) : boolean {
const support = this.featureStates[feature];
if(!support) {
return false;
}
if(support.support === "supported" || support.support === "experimental" || support.support === "deprecated") {
return typeof version === "number" ? version >= support.version : true;
}
return false;
}
awaitFeatures() : Promise<boolean> {
if(this.featureAwait) {
return this.featureAwait;
} else if(this.featuresSet) {
return Promise.resolve(true);
}
return this.featureAwait = new Promise<boolean>(resolve => this.featureAwaitCallback = resolve);
}
listenSupportChange(feature: ServerFeature, listener: (support: boolean) => void, version?: number) : () => void {
return this.events.on("notify_state_changed", event => {
if(event.feature !== feature) {
return;
}
listener(this.supportsFeature(feature, version));
});
}
private disableAllFeatures() {
for(const feature of Object.keys(this.featureStates) as ServerFeature[]) {
this.setFeatureSupport(feature, "unsupported");
}
}
private setFeatureSupport(feature: ServerFeature, support: ServerFeatureSupport, version?: number) {
logTrace(LogCategory.SERVER, tr("Setting server feature %s to %s (version %d)"), feature, support, version);
if(support === "unsupported") {
if(!this.featureStates[feature]) {
return;
}
delete this.featureStates[feature];
this.events.fire("notify_state_changed", { feature: feature, support: "unsupported" });
} else {
if(!this.featureStates[feature] || this.featureStates[feature].version !== version || this.featureStates[feature].support !== support) {
this.featureStates[feature] = {
support: support,
version: version
};
this.events.fire("notify_state_changed", {
feature: feature,
support: support,
version: version
});
}
}
}
}