2018-08-29 15:49:44 +02:00
|
|
|
import { Injectable, NgZone, EventEmitter } from '@angular/core';
|
2018-06-28 17:11:04 +02:00
|
|
|
import { Router } from '@angular/router';
|
2018-08-29 13:21:25 +02:00
|
|
|
import { Observable, Subject } from 'rxjs';
|
2018-08-28 11:07:10 +02:00
|
|
|
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar } from '@angular/material';
|
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
2018-08-23 15:28:57 +02:00
|
|
|
|
2018-08-28 11:07:10 +02:00
|
|
|
/**
|
|
|
|
* A key value mapping for params, that should be appendet to the url on a new connection.
|
|
|
|
*/
|
2018-08-24 13:05:03 +02:00
|
|
|
interface QueryParams {
|
|
|
|
[key: string]: string;
|
|
|
|
}
|
|
|
|
|
2018-08-28 11:07:10 +02:00
|
|
|
/**
|
|
|
|
* The generic message format in which messages are send and recieved by the server.
|
|
|
|
*/
|
2018-08-23 15:28:57 +02:00
|
|
|
interface WebsocketMessage {
|
|
|
|
type: string;
|
|
|
|
content: any;
|
|
|
|
id: string;
|
|
|
|
}
|
2018-06-28 17:11:04 +02:00
|
|
|
|
2018-07-12 14:11:31 +02:00
|
|
|
/**
|
2018-08-28 11:07:10 +02:00
|
|
|
* Service that handles WebSocket connections. Other services can register themselfs
|
|
|
|
* with {@method getOberservable} for a specific type of messages. The content will be published.
|
2018-07-12 14:11:31 +02:00
|
|
|
*/
|
2018-06-28 17:11:04 +02:00
|
|
|
@Injectable({
|
|
|
|
providedIn: 'root'
|
|
|
|
})
|
|
|
|
export class WebsocketService {
|
2018-07-12 14:11:31 +02:00
|
|
|
/**
|
2018-08-28 11:07:10 +02:00
|
|
|
* The reference to the snackbar entry that is shown, if the connection is lost.
|
2018-07-12 14:11:31 +02:00
|
|
|
*/
|
2018-08-28 11:07:10 +02:00
|
|
|
private connectionErrorNotice: MatSnackBarRef<SimpleSnackBar>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Subjects that will be called, if a reconnect was successful.
|
|
|
|
*/
|
2018-08-29 15:49:44 +02:00
|
|
|
private _reconnectEvent: EventEmitter<void> = new EventEmitter<void>();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Getter for the reconnect event.
|
|
|
|
*/
|
|
|
|
public get reconnectEvent(): EventEmitter<void> {
|
|
|
|
return this._reconnectEvent;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Listeners will be nofitied, if the wesocket connection is establiched.
|
|
|
|
*/
|
|
|
|
private _connectEvent: EventEmitter<void> = new EventEmitter<void>();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Getter for the connect event.
|
|
|
|
*/
|
|
|
|
public get connectEvent(): EventEmitter<void> {
|
|
|
|
return this._connectEvent;
|
|
|
|
}
|
2018-08-28 11:07:10 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The websocket.
|
|
|
|
*/
|
|
|
|
private websocket: WebSocket;
|
2018-08-23 15:28:57 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Subjects for types of websocket messages. A subscriber can get an Observable by {@function getOberservable}.
|
|
|
|
*/
|
2018-08-24 13:05:03 +02:00
|
|
|
private subjects: { [type: string]: Subject<any> } = {};
|
2018-06-28 17:11:04 +02:00
|
|
|
|
2018-08-29 13:21:25 +02:00
|
|
|
/**
|
|
|
|
* Constructor that handles the router
|
|
|
|
* @param router the URL Router
|
|
|
|
*/
|
|
|
|
public constructor(
|
|
|
|
private router: Router,
|
|
|
|
private matSnackBar: MatSnackBar,
|
|
|
|
private zone: NgZone,
|
|
|
|
public translate: TranslateService
|
2018-08-29 15:49:44 +02:00
|
|
|
) {}
|
2018-08-29 13:21:25 +02:00
|
|
|
|
2018-07-12 14:11:31 +02:00
|
|
|
/**
|
2018-08-28 11:07:10 +02:00
|
|
|
* Creates a new WebSocket connection and handles incomming events.
|
2018-07-12 14:11:31 +02:00
|
|
|
*
|
2018-08-28 11:07:10 +02:00
|
|
|
* Uses NgZone to let all callbacks run in the angular context.
|
2018-07-12 14:11:31 +02:00
|
|
|
*/
|
2018-08-28 11:07:10 +02:00
|
|
|
public connect(retry = false, changeId?: number): void {
|
|
|
|
if (this.websocket) {
|
|
|
|
return;
|
|
|
|
}
|
2018-08-24 13:05:03 +02:00
|
|
|
const queryParams: QueryParams = {};
|
|
|
|
// comment-in if changes IDs are supported on server side.
|
|
|
|
/*if (changeId !== undefined) {
|
|
|
|
queryParams.changeId = changeId.toString();
|
|
|
|
}*/
|
|
|
|
|
2018-08-28 11:07:10 +02:00
|
|
|
// Create the websocket
|
2018-07-12 14:11:31 +02:00
|
|
|
const socketProtocol = this.getWebSocketProtocol();
|
2018-06-28 17:11:04 +02:00
|
|
|
const socketServer = window.location.hostname + ':' + window.location.port;
|
2018-08-24 13:05:03 +02:00
|
|
|
const socketPath = this.getWebSocketPath(queryParams);
|
2018-08-28 11:07:10 +02:00
|
|
|
this.websocket = new WebSocket(socketProtocol + socketServer + socketPath);
|
|
|
|
|
|
|
|
// connection established. If this connect attept was a retry,
|
|
|
|
// The error notice will be removed and the reconnectSubject is published.
|
|
|
|
this.websocket.onopen = (event: Event) => {
|
|
|
|
this.zone.run(() => {
|
|
|
|
if (retry) {
|
|
|
|
if (this.connectionErrorNotice) {
|
|
|
|
this.connectionErrorNotice.dismiss();
|
|
|
|
this.connectionErrorNotice = null;
|
|
|
|
}
|
2018-08-29 15:49:44 +02:00
|
|
|
this._reconnectEvent.emit();
|
2018-08-28 11:07:10 +02:00
|
|
|
}
|
2018-08-29 15:49:44 +02:00
|
|
|
this._connectEvent.emit();
|
2018-08-28 11:07:10 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
this.websocket.onmessage = (event: MessageEvent) => {
|
|
|
|
this.zone.run(() => {
|
|
|
|
const message: WebsocketMessage = JSON.parse(event.data);
|
2018-08-23 15:28:57 +02:00
|
|
|
const type: string = message.type;
|
|
|
|
if (type === 'error') {
|
|
|
|
console.error('Websocket error', message.content);
|
2018-08-24 13:05:03 +02:00
|
|
|
} else if (this.subjects[type]) {
|
2018-08-28 11:07:10 +02:00
|
|
|
// Pass the content to the registered subscribers.
|
2018-08-24 13:05:03 +02:00
|
|
|
this.subjects[type].next(message.content);
|
2018-08-23 15:28:57 +02:00
|
|
|
} else {
|
|
|
|
console.log(`Got unknown websocket message type "${type}" with content`, message.content);
|
|
|
|
}
|
|
|
|
});
|
2018-08-28 11:07:10 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
this.websocket.onclose = (event: CloseEvent) => {
|
|
|
|
this.zone.run(() => {
|
|
|
|
this.websocket = null;
|
|
|
|
if (event.code !== 1000) {
|
|
|
|
// 1000 is a normal close, like the close on logout
|
|
|
|
if (!this.connectionErrorNotice) {
|
|
|
|
// So here we have a connection failure that wasn't intendet.
|
|
|
|
this.connectionErrorNotice = this.matSnackBar.open(
|
|
|
|
this.translate.instant('Offline mode: You can use OpenSlides but changes are not saved.'),
|
|
|
|
'',
|
|
|
|
{ duration: 0 }
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// A random retry timeout between 2000 and 5000 ms.
|
|
|
|
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
|
|
|
setTimeout(() => {
|
|
|
|
this.connect((retry = true));
|
|
|
|
}, timeout);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the websocket connection.
|
|
|
|
*/
|
|
|
|
public close(): void {
|
|
|
|
if (this.websocket) {
|
|
|
|
this.websocket.close();
|
|
|
|
this.websocket = null;
|
2018-08-23 15:28:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an observable for messages of the given type.
|
|
|
|
* @param type the message type
|
|
|
|
*/
|
|
|
|
public getOberservable<T>(type: string): Observable<T> {
|
2018-08-24 13:05:03 +02:00
|
|
|
if (!this.subjects[type]) {
|
|
|
|
this.subjects[type] = new Subject<T>();
|
2018-08-23 15:28:57 +02:00
|
|
|
}
|
2018-08-24 13:05:03 +02:00
|
|
|
return this.subjects[type].asObservable();
|
2018-08-23 15:28:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a message to the server with the content and the given type.
|
|
|
|
*
|
|
|
|
* @param type the message type
|
|
|
|
* @param content the actual content
|
|
|
|
*/
|
2018-08-28 11:07:10 +02:00
|
|
|
public send<T>(type: string, content: T, id?: string): void {
|
|
|
|
if (!this.websocket) {
|
2018-08-23 15:28:57 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const message: WebsocketMessage = {
|
|
|
|
type: type,
|
|
|
|
content: content,
|
2018-08-28 11:07:10 +02:00
|
|
|
id: id
|
2018-08-23 15:28:57 +02:00
|
|
|
};
|
|
|
|
|
2018-08-28 11:07:10 +02:00
|
|
|
// create message id if not given. Required by the server.
|
|
|
|
if (!message.id) {
|
|
|
|
message.id = '';
|
|
|
|
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
message.id += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
|
|
}
|
2018-06-28 17:11:04 +02:00
|
|
|
}
|
2018-08-28 11:07:10 +02:00
|
|
|
this.websocket.send(JSON.stringify(message));
|
2018-06-28 17:11:04 +02:00
|
|
|
}
|
|
|
|
|
2018-07-12 14:11:31 +02:00
|
|
|
/**
|
|
|
|
* Delegates to socket-path for either the side or projector websocket.
|
|
|
|
*/
|
2018-08-24 13:05:03 +02:00
|
|
|
private getWebSocketPath(queryParams: QueryParams = {}): string {
|
2018-08-29 13:21:25 +02:00
|
|
|
// currentRoute does not end with '/'
|
2018-06-28 17:11:04 +02:00
|
|
|
const currentRoute = this.router.url;
|
2018-08-24 13:05:03 +02:00
|
|
|
let path: string;
|
2018-06-28 17:11:04 +02:00
|
|
|
if (currentRoute.includes('/projector') || currentRoute.includes('/real-projector')) {
|
2018-08-24 13:05:03 +02:00
|
|
|
path = '/ws/projector/';
|
2018-06-28 17:11:04 +02:00
|
|
|
} else {
|
2018-08-24 13:05:03 +02:00
|
|
|
path = '/ws/site/';
|
|
|
|
}
|
|
|
|
|
|
|
|
const keys: string[] = Object.keys(queryParams);
|
|
|
|
if (keys.length > 0) {
|
|
|
|
path += keys
|
|
|
|
.map(key => {
|
|
|
|
return key + '=' + queryParams[key];
|
|
|
|
})
|
|
|
|
.join('&');
|
2018-06-28 17:11:04 +02:00
|
|
|
}
|
2018-08-24 13:05:03 +02:00
|
|
|
return path;
|
2018-06-28 17:11:04 +02:00
|
|
|
}
|
|
|
|
|
2018-07-12 14:11:31 +02:00
|
|
|
/**
|
|
|
|
* returns the desired websocket protocol
|
|
|
|
*/
|
|
|
|
private getWebSocketProtocol(): string {
|
2018-06-28 17:11:04 +02:00
|
|
|
if (location.protocol === 'https') {
|
|
|
|
return 'wss://';
|
|
|
|
} else {
|
|
|
|
return 'ws://';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|