landstrasse
Version:
Strongly typed WAMP Client for browsers
569 lines • 22.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const transport_1 = require("./transport");
const json_1 = require("./serializer/json");
const deferred_1 = require("./util/deferred");
const logger_1 = require("./util/logger");
const ConnectionOpenError_1 = require("./error/ConnectionOpenError");
const Transport_1 = require("./types/Transport");
const id_1 = require("./util/id");
const MessageTypes_1 = require("./types/messages/MessageTypes");
const util_1 = require("./util");
const Connection_1 = require("./types/Connection");
const connection_1 = require("./state/connection");
const publisher_1 = require("./processor/publisher");
const subscriber_1 = require("./processor/subscriber");
const callee_1 = require("./processor/callee");
const caller_1 = require("./processor/caller");
const PROCESSOR_FACTORIES = [
publisher_1.default,
subscriber_1.default,
caller_1.default,
callee_1.default
];
const createIdGenerators = () => ({
global: new id_1.GlobalIDGenerator(),
session: new id_1.SessionIDGenerator(),
});
class Connection {
constructor(endpoint, realm, options) {
var _a, _b, _c, _d, _e, _f, _g, _h;
Object.defineProperty(this, "_options", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_endpoint", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_realm", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_serializer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_processors", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_sessionId", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_transport", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_state", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_openingDeferred", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_openedDeferred", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_closedDeferred", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_closeRequested", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_isRetrying", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "_retryTimer", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "_maxRetries", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_retryCount", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "_retryDelayInitial", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_retryDelay", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_retryDelayMax", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_logger", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this._endpoint = endpoint;
this._realm = realm;
this._options = Object.assign({ retryIfUnreachable: true }, options);
this._serializer = (_b = (_a = this._options) === null || _a === void 0 ? void 0 : _a.serializer) !== null && _b !== void 0 ? _b : new json_1.default();
this._maxRetries = (_d = (_c = this._options) === null || _c === void 0 ? void 0 : _c.maxRetries) !== null && _d !== void 0 ? _d : -1;
this._retryDelayInitial = (_f = (_e = this._options) === null || _e === void 0 ? void 0 : _e.initialRetryDelay) !== null && _f !== void 0 ? _f : 3;
this._retryDelayMax = (_h = (_g = this._options) === null || _g === void 0 ? void 0 : _g.maxRetryDelay) !== null && _h !== void 0 ? _h : 60;
this._retryDelay = this._retryDelayInitial;
this._state = new connection_1.ConnectionStateMachine();
this._logger = new logger_1.default(options.logFunction, !!options.debug);
}
get endpoint() {
return this._endpoint;
}
get realm() {
return this._realm;
}
get sessionId() {
return this._sessionId;
}
get isConnected() {
return this._state.current === connection_1.EConnectionState.ESTABLISHED;
}
get isConnecting() {
return !!this._openingDeferred;
}
get isRetrying() {
return this._isRetrying;
}
get onOpen() {
if (this.isConnected) {
return Promise.resolve();
}
if (!this._openedDeferred) {
this._openedDeferred = new deferred_1.default();
}
return this._openedDeferred.promise;
}
open() {
if (this._openingDeferred) {
return this._openingDeferred.promise;
}
if (this.isConnected || this._transport) {
return Promise.reject('Connection already opened or opening.');
}
this.resetRetry();
this._closeRequested = false;
this._logger.log(logger_1.LogLevel.DEBUG, 'Opening Connection.');
const deferred = this._openingDeferred = new deferred_1.default();
this._open();
return deferred.promise;
}
close() {
if (!this._transport && !this.isRetrying) {
return Promise.reject('Connection already closed.');
}
if (this._closedDeferred) {
return this._closedDeferred.promise;
}
const deferred = this._closedDeferred = new deferred_1.default();
// - The app wants to close .. don't retry.
this._closeRequested = true;
if (this._transport) {
this._transport.send([
MessageTypes_1.EWampMessageID.GOODBYE,
{ message: 'client shutdown' },
'wamp.close.normal',
]);
this._logger.log(logger_1.LogLevel.DEBUG, 'Closing Connection.');
this._state.update([connection_1.EMessageDirection.SENT, MessageTypes_1.EWampMessageID.GOODBYE]);
}
else {
this.handleClose({
reason: 'wamp.close.normal',
message: 'Client shutdown (between two retries).',
wasClean: true,
});
}
return deferred.promise;
}
//
// - WAMP methods.
//
call(uri, args, kwargs, opts) {
if (!this._processors) {
return [
Promise.reject('Invalid session state.'),
() => Promise.resolve(),
];
}
return this._processors[2].call(uri, args, kwargs, opts);
}
register(uri, handler, opts) {
if (!this._processors) {
return Promise.reject('Invalid session state.');
}
return this._processors[3].register(uri, handler, opts);
}
subscribe(uri, handler, opts) {
if (!this._processors) {
return Promise.reject('Invalid session state.');
}
return this._processors[1].subscribe(uri, handler, opts);
}
publish(uri, args, kwargs, opts) {
if (!this._processors) {
return Promise.reject('Invalid session state.');
}
return this._processors[0].publish(uri, args, kwargs, opts);
}
//
// - Processors.
//
processSessionMessage(msg) {
var _a, _b, _c;
if (!this._transport) {
return;
}
this._state.update([connection_1.EMessageDirection.RECEIVED, msg[0]]);
switch (this._state.current) {
case connection_1.EConnectionState.CHALLENGING: {
const challengeMsg = msg;
if (!('authProvider' in this._options) || !this._options.authProvider) {
this._logger.log(logger_1.LogLevel.ERROR, 'Received WAMP challenge, but no auth provider set.');
this._transport.close(3000, 'auth_error', 'Received WAMP challenge, but no auth provider set.');
return;
}
this._options.authProvider
.computeChallenge(challengeMsg[2] || {})
.then((signature) => {
if (!this._transport) {
return;
}
return this._transport.send([
MessageTypes_1.EWampMessageID.AUTHENTICATE,
signature.signature,
signature.details || {},
]);
})
.then(() => {
this._state.update([
connection_1.EMessageDirection.SENT,
MessageTypes_1.EWampMessageID.AUTHENTICATE,
]);
})
.catch((error) => {
if (!this._transport) {
return;
}
this._logger.log(logger_1.LogLevel.ERROR, 'Failed to compute challenge.', error);
this._transport.close(3000, 'auth_challenge_failed', 'Failed to compute challenge.');
});
break;
}
case connection_1.EConnectionState.ESTABLISHED: {
const idGenerators = createIdGenerators();
this._processors = PROCESSOR_FACTORIES.map((procssorClass) => {
return new procssorClass((msg) => this._transport.send(msg), (reason) => { this.handleProtocolViolation(reason); }, idGenerators, this._logger);
});
const [, sessionId, welcomeDetails] = msg;
this._sessionId = sessionId !== null ? sessionId : -1;
this._logger.log(logger_1.LogLevel.DEBUG, `Connection established.`, welcomeDetails);
this.handleOpen(welcomeDetails);
break;
}
case connection_1.EConnectionState.CLOSING: {
// - We received a GOODBYE message from the server, so we reply with goodbye and shutdown the transport.
this._transport.send([
MessageTypes_1.EWampMessageID.GOODBYE,
{ message: 'clean close' },
'wamp.close.goodbye_and_out',
]);
this._state.update([connection_1.EMessageDirection.SENT, MessageTypes_1.EWampMessageID.GOODBYE]);
this._transport.close(1000, 'wamp.close.normal', (_a = msg[1]) === null || _a === void 0 ? void 0 : _a.message);
break;
}
case connection_1.EConnectionState.CLOSED: {
// - Clean close finished, actually close the transport, so `closed` and close callbacks will be created.
const message = msg[0] === MessageTypes_1.EWampMessageID.GOODBYE ? (_b = msg[1]) === null || _b === void 0 ? void 0 : _b.message : undefined;
this._transport.close(1000, 'wamp.close.normal', message);
break;
}
case connection_1.EConnectionState.ERROR: {
// - Protocol violation, so close the transport not clean (i.e. code 3000)
// and if we encountered the error, send an ABORT message to the server.
if (msg[0] !== MessageTypes_1.EWampMessageID.ABORT) {
this.handleProtocolViolation('Protocol violation during session creation.');
}
else {
const { message } = (_c = msg[1]) !== null && _c !== void 0 ? _c : {};
this._transport.close(3000, msg[2], message);
}
break;
}
}
}
processMessage(msg) {
if (msg[0] === MessageTypes_1.EWampMessageID.GOODBYE) {
this._state.update([connection_1.EMessageDirection.RECEIVED, msg[0]]);
return;
}
const handled = this._processors.some((processor) => processor.processMessage(msg));
if (!handled) {
this._logger.log(logger_1.LogLevel.ERROR, `Unhandled message.`, msg);
this.handleProtocolViolation('No handler found for message.');
}
}
sendHello() {
const details = {
roles: Object.assign({}, ...PROCESSOR_FACTORIES.map((processor) => processor.getFeatures())),
};
if ('authProvider' in this._options && this._options.authProvider) {
details.authid = this._options.authProvider.authId;
details.authmethods = [this._options.authProvider.authMethod];
}
if ('auth' in this._options && this._options.auth) {
if (this._options.auth.id) {
details.authid = this._options.auth.id;
}
if (this._options.auth.method) {
details.authmethods = [this._options.auth.method];
}
if (this._options.auth.extra) {
details.authextra = this._options.auth.extra;
}
}
const message = [MessageTypes_1.EWampMessageID.HELLO, this.realm, details];
this._transport.send(message).then(() => { this._state.update([connection_1.EMessageDirection.SENT, MessageTypes_1.EWampMessageID.HELLO]); }, (err) => { this.handleProtocolViolation(`Transport error: ${err}.`); });
}
//
// - Handlers.
//
handleTransportEvent(event) {
switch (event.type) {
case Transport_1.ETransportEventType.OPEN: {
this.sendHello();
break;
}
case Transport_1.ETransportEventType.MESSAGE: {
if (this.isConnected) {
this.processMessage(event.message);
}
else {
this.processSessionMessage(event.message);
}
break;
}
case Transport_1.ETransportEventType.CRITICAL_ERROR:
case Transport_1.ETransportEventType.ERROR: {
if (event.type === Transport_1.ETransportEventType.CRITICAL_ERROR || this.isConnecting) {
if (this._transport.isOpen) {
this._transport.close(3000, 'connection_error', event.error);
}
else {
this.resetConnectionInfos();
this.handleClose({
reason: 'connection_error',
message: event.error,
wasClean: true,
});
}
}
else {
this._logger.log(logger_1.LogLevel.WARNING, 'Transport error.', event.error);
}
break;
}
case Transport_1.ETransportEventType.CLOSE: {
this.resetConnectionInfos();
this.handleClose({
code: event.code,
reason: event.reason,
message: event.message,
wasClean: event.wasClean,
});
break;
}
}
}
handleProtocolViolation(message) {
if (!this._transport) {
this._logger.log(logger_1.LogLevel.ERROR, 'Failed to handle protocol violation: Already closed.');
return;
}
const abortMessage = [
MessageTypes_1.EWampMessageID.ABORT,
{ message },
'wamp.error.protocol_violation',
];
this._logger.log(logger_1.LogLevel.ERROR, `Protocol violation: ${message}.`);
this._transport.send(abortMessage);
this._transport.close(3000, 'protocol_violation', message);
}
handleOpen(details) {
var _a, _b, _c, _d, _e, _f, _g;
if (!this.isConnecting) {
return false;
}
this.resetRetryTimer();
if (!(details instanceof Error)) {
this.resetRetry();
(_b = (_a = this._options).onOpen) === null || _b === void 0 ? void 0 : _b.call(_a, details);
(_c = this._openingDeferred) === null || _c === void 0 ? void 0 : _c.resolve(details);
this._openingDeferred = null;
(_d = this._openedDeferred) === null || _d === void 0 ? void 0 : _d.resolve();
this._openedDeferred = null;
return true;
}
this._logger.log(logger_1.LogLevel.WARNING, 'Connection failed.', details);
const stopReconnecting = !!((_f = (_e = this._options).onOpenError) === null || _f === void 0 ? void 0 : _f.call(_e, details));
if (!stopReconnecting && this.retryOpening()) {
return true;
}
(_g = this._openingDeferred) === null || _g === void 0 ? void 0 : _g.reject(details);
this._openingDeferred = null;
return true;
}
handleClose(details) {
var _a, _b, _c;
const wasRequested = this._closeRequested;
const connectionError = new ConnectionOpenError_1.default(details.reason, details.message);
if (!wasRequested && this.handleOpen(connectionError)) {
return;
}
this.resetRetryTimer();
const reason = !details.wasClean
? (this.isConnecting ? Connection_1.CloseReason.UNREACHABLE : Connection_1.CloseReason.LOST)
: Connection_1.CloseReason.CLOSED;
this._logger.log(logger_1.LogLevel[wasRequested ? 'DEBUG' : 'WARNING'], 'Connection closed.', details);
const stopReconnecting = !!((_b = (_a = this._options).onClose) === null || _b === void 0 ? void 0 : _b.call(_a, reason, details));
if (!stopReconnecting && this.retryOpening()) {
return;
}
if (this.isConnecting) {
(_c = this._openingDeferred) === null || _c === void 0 ? void 0 : _c.reject(connectionError);
this._openingDeferred = null;
}
if (this._closedDeferred) {
this._closedDeferred.resolve();
this._closedDeferred = null;
}
}
//
// - Internal
//
_open() {
if (this.isConnected || this._transport) {
return;
}
if (!this._openingDeferred) {
this._openingDeferred = new deferred_1.default();
}
this._state = new connection_1.ConnectionStateMachine();
this._transport = new transport_1.default(this._serializer);
this._transport.open(this.endpoint, this.handleTransportEvent.bind(this));
}
retryOpening() {
if (this._closeRequested) {
return false;
}
// - Closed while connecting.
if (this.isConnecting && !this._options.retryIfUnreachable) {
this._logger.log(logger_1.LogLevel.WARNING, 'Auto-reconnect disabled!');
return false;
}
const nextTry = this.nextTryInfos();
if (!nextTry.willRetry) {
this._logger.log(logger_1.LogLevel.WARNING, 'Giving up trying to reconnect!');
return false;
}
const retry = () => {
if (this._closeRequested) {
return;
}
this._open();
};
this._isRetrying = true;
this._retryTimer = setTimeout(retry, nextTry.delay * 1000);
this._logger.log(logger_1.LogLevel.INFO, `Will try to reconnect [${nextTry.count}] in ${nextTry.delay}s ...`);
return true;
}
resetRetryTimer() {
if (this._retryTimer) {
clearTimeout(this._retryTimer);
}
this._retryTimer = null;
}
resetRetry() {
this.resetRetryTimer();
this._retryCount = 0;
this._retryDelay = this._retryDelayInitial;
this._isRetrying = false;
}
nextTryInfos() {
this._retryDelay = util_1.normalRand(this._retryDelay, this._retryDelay * 0.1);
if (this._retryDelay > this._retryDelayMax) {
this._retryDelay = this._retryDelayMax;
}
this._retryCount += 1;
let infos = { count: null, delay: null, willRetry: false };
if (!this._closeRequested && (this._maxRetries === -1 || this._retryCount <= this._maxRetries)) {
infos = { count: this._retryCount, delay: this._retryDelay, willRetry: true };
}
// - Retry delay growth for next retry cycle.
this._retryDelay = this._retryDelay * 1.5;
return infos;
}
resetConnectionInfos() {
this._transport = null;
this._sessionId = null;
this._state = new connection_1.ConnectionStateMachine();
if (this._processors) {
this._processors.forEach((processor) => processor.close());
this._processors = null;
}
}
}
exports.default = Connection;
//# sourceMappingURL=connection.js.map