UNPKG

landstrasse

Version:

Strongly typed WAMP Client for browsers

569 lines 22.5 kB
"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