kuzzle-sdk
Version:
Official Javascript SDK for Kuzzle
282 lines • 11.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const KuzzleError_1 = require("../KuzzleError");
const Realtime_1 = require("./abstract/Realtime");
const Http_1 = __importDefault(require("./Http"));
const DisconnectionOrigin_1 = require("./DisconnectionOrigin");
/**
* WebSocket protocol used to connect to a Kuzzle server.
*/
class WebSocketProtocol extends Realtime_1.BaseProtocolRealtime {
/**
* @param host Kuzzle server hostname or IP
* @param options WebSocket connection options
* - `autoReconnect` Automatically reconnect to kuzzle after a `disconnected` event. (default: `true`)
* - `port` Kuzzle server port (default: `7512`)
* - `headers` Connection custom HTTP headers (Not supported by browsers)
* - `reconnectionDelay` Number of milliseconds between reconnection attempts (default: `1000`)
* - `pingInterval` Number of milliseconds between two pings (default: `2000`)
* - `ssl` Use SSL to connect to Kuzzle server. Default `false` unless port is 443 or 7443.
*/
constructor(host, options = {}) {
super(host, options, "ws");
if (typeof host !== "string" || host === "") {
throw new Error("host is required");
}
// Browsers WebSocket API
if (typeof WebSocket !== "undefined") {
this.WebSocketClient = WebSocket;
// There are no options allowed in the browsers WebSocket API
this.options = null;
}
else {
this.WebSocketClient = require("ws");
this.options = {
headers: options.headers || null,
perMessageDeflate: false,
};
if (this.options.headers !== null &&
(Array.isArray(this.options.headers) ||
typeof this.options.headers !== "object")) {
throw new Error('Invalid "headers" option: expected an object');
}
}
this._pingInterval =
typeof options.pingInterval === "number" ? options.pingInterval : 2000;
this.client = null;
this.lasturl = null;
}
/**
* Connect to the websocket server
*/
_connect() {
return new Promise((resolve, reject) => {
const url = `${this.ssl ? "wss" : "ws"}://${this.host}:${this.port}`;
super.connect();
if (url !== this.lasturl) {
this.wasConnected = false;
this.lasturl = url;
}
this.client = new this.WebSocketClient(url, this.options);
/**
* Defining behavior depending on the Websocket client type
* Which can be the browser or node one.
*/
if (typeof WebSocket !== "undefined") {
this.ping = () => {
this.client.send('{"p":1}');
};
}
else {
this.ping = () => {
this.client.ping();
};
this.client.on("pong", () => {
this.waitForPong = false;
});
}
this.client.onopen = () => {
this.clientConnected();
this.setupPingPong();
return resolve();
};
this.client.onclose = (closeEvent, message) => {
let status, reason = message;
if (typeof closeEvent === "number") {
status = closeEvent;
}
else {
status = closeEvent.code;
if (closeEvent.reason) {
reason = closeEvent.reason;
}
}
if (status === 1000) {
this.clientDisconnected(DisconnectionOrigin_1.DisconnectionOrigin.USER_CONNECTION_CLOSED);
}
// do not forward a connection close error if no
// connection has been previously established
else if (this.wasConnected) {
const error = new Error(reason);
error.status = status;
this.clientNetworkError(error);
}
};
this.client.onerror = (error) => {
let err = error;
if (!(error instanceof Error)) {
// browser-side, the payload sent to this event is a generic "Event"
// object bearing no information about the cause of the error
err =
error && (typeof Event === "undefined" || !(error instanceof Event))
? new Error(error.message || error)
: new Error("Connection error");
}
this.clientNetworkError(err);
if ([this.client.CLOSING, this.client.CLOSED].indexOf(this.client.readyState) > -1) {
return reject(err);
}
};
this.client.onmessage = (payload) => {
const data = JSON.parse(payload.data || payload);
/**
* Since Kuzzle 2.10.0
* Corresponds to a custom pong response message
*/
if (data && data.p && data.p === 2 && Object.keys(data).length === 1) {
this.waitForPong = false;
return;
}
// for responses, data.room == requestId
if (data.type === "TokenExpired") {
this.emit("tokenExpired");
}
else if (data.room) {
this.emit(data.room, data);
}
else {
// @deprecated
this.emit("discarded", data);
const error = new KuzzleError_1.KuzzleError(data.error, new Error().stack, this.constructor.name);
this.emit("queryError", { error, request: data });
}
/**
* In case you're running a Kuzzle version under 2.10.0
* The response from a browser custom ping will be another payload.
* We need to clear this timeout at each message to keep
* the connection alive if it's the case
*/
this.waitForPong = false;
};
});
}
connect() {
if (this.cookieSupport) {
return this._httpProtocol.connect().then(() => this._connect());
}
return this._connect();
}
enableCookieSupport() {
if (typeof XMLHttpRequest === "undefined") {
throw new Error("Support for cookie cannot be enabled outside of a browser");
}
super.enableCookieSupport();
this._httpProtocol = new Http_1.default(this.host, {
port: this.port,
ssl: this.ssl,
});
this._httpProtocol.enableCookieSupport();
}
/**
* Sends a payload to the connected server
*
* @param {Object} payload
*/
send(request, options = {}) {
if (!this.client || this.client.readyState !== this.client.OPEN) {
return;
}
if (!this.cookieSupport ||
request.controller !== "auth" ||
(request.action !== "login" &&
request.action !== "logout" &&
request.action !== "refreshToken")) {
this.client.send(JSON.stringify(request));
return;
}
const formattedRequest = this._httpProtocol.formatRequest(request, options);
if (!formattedRequest) {
return;
}
this.emit("websocketRenewalStart"); // Notify that the websocket is going to renew his connection with Kuzzle
if (this.client) {
this.client.onclose = undefined; // Remove the listener that will emit disconnected / networkError event before closing
this.client.close(1000);
}
this.client = null;
this.clientDisconnected(DisconnectionOrigin_1.DisconnectionOrigin.WEBSOCKET_AUTH_RENEWAL); // Simulate a disconnection, this will enable offline queue and trigger realtime subscriptions backup
this._httpProtocol
._sendHttpRequest(formattedRequest)
.then((response) => {
// Reconnection
return this.connect().then(() => {
this.emit(formattedRequest.payload.requestId, response);
this.emit("websocketRenewalDone"); // Notify that the websocket has finished renewing his connection with Kuzzle
});
})
.catch((error) => {
this.emit(formattedRequest.payload.requestId, { error });
this.emit("websocketRenewalDone"); // Notify that the websocket has finished renewing his connection with Kuzzle
});
}
/**
* @override
*/
clientDisconnected(origin) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
super.clientDisconnected(origin);
}
/**
* @override
*
* @param {Error} error
*/
clientNetworkError(error) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
super.clientNetworkError(error);
}
/**
* Closes the connection
*/
close() {
this.state = "offline";
this.wasConnected = false;
if (this.client) {
this.client.close(1000); // Close with 1000 will trigger the `disconnect`
}
// Remove all listerner after closing the connection, this way the `disconnect` can be emitted when calling close.
this.removeAllListeners();
this.client = null;
this.stopRetryingToConnect = true;
clearInterval(this.pingIntervalId);
this.pingIntervalId = null;
super.close();
}
setupPingPong() {
clearInterval(this.pingIntervalId);
// Reset when connection is established
this.waitForPong = false;
this.pingIntervalId = setInterval(() => {
// If the connection is established and we are not waiting for a pong we ping Kuzzle
if (this.client &&
this.client.readyState === this.client.OPEN &&
!this.waitForPong) {
this.ping();
this.waitForPong = true;
return;
}
// If we were waiting for a pong that never occured before the next ping cycle we throw an error
if (this.waitForPong) {
const error = new Error("Kuzzle does'nt respond to ping. Connection lost.");
error.status = 503;
/**
* Ensure that the websocket connection is closed because if the connection
* was fine but Kuzzle could not respond in time a new connection will be
* created if `autoReconnect=true` and there would 2 opened websocket connection.
*/
clearInterval(this.pingIntervalId);
this.client.onclose = undefined; // Remove the listener that will emit disconnected / networkError event before closing
this.client.close(1000);
this.waitForPong = false;
this.clientNetworkError(error);
}
}, this._pingInterval);
}
}
exports.default = WebSocketProtocol;
//# sourceMappingURL=WebSocket.js.map