UNPKG

@n1k1t/mock-server

Version:

The ultimate toolkit to intercept, transform, and simulate HTTP/WS traffic with type-safe expectations

177 lines 7.37 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebSocket = void 0; const qs_1 = __importDefault(require("qs")); const ws_1 = require("ws"); const rxjs_1 = require("rxjs"); const errors_1 = require("./errors"); const message_1 = require("../message"); const utils_1 = require("../../../utils"); const logger_1 = require("../../../logger"); const logger = logger_1.Logger.build('Ws'); const webSocketClosingStates = [ ws_1.WebSocket.CLOSED, ws_1.WebSocket.CLOSING ]; class WebSocket { constructor(url, configuration) { this.url = url; this.configuration = configuration; this.observable = new rxjs_1.Subject(); this.nested = { headers: (0, utils_1.cast)(null), source: (0, utils_1.cast)(null), status: (0, utils_1.cast)(null), error: (0, utils_1.cast)(null), }; } get error() { return this.nested.error; } async headers() { if (this.nested.headers) { return this.nested.headers; } if (!this.nested.source) { return {}; } if (webSocketClosingStates.includes(this.nested.source.readyState)) { return {}; } const headers = await Promise.race([ new Promise((resolve) => this.nested.source.once('close', () => resolve(null))), new Promise((resolve) => this.nested.source.once('upgrade', (request) => resolve(request.headers))), ]); return headers ?? {}; } async status() { if (this.nested.status !== null) { return this.nested.status; } if (!this.nested.source) { return 1000; } return new Promise((resolve) => this.nested.source.once('close', (code) => resolve(code))); } subscribe(observer) { this.observable.subscribe({ error: observer.error ?? (() => null), next: observer.next, complete: observer.complete, }); return this; } forEach(handler) { return this.observable.forEach(handler); } waitUntilComplete() { return new Promise((resolve) => this.subscribe({ complete: resolve })); } checkIsConnected() { return this.nested.source?.readyState === 1; } async send(payload) { await this.resolve(); if (!this.checkIsConnected()) { throw errors_1.WebSocketConnectionError.build(this, 'Is not connected anymore'); } return new Promise((resolve, reject) => this.nested.source?.send(typeof payload === 'object' ? JSON.stringify(payload) : String(payload), (error) => error ? reject(error) : resolve())); } connect() { try { this.nested.source = new ws_1.WebSocket(this.url, this.configuration); this.nested.source.once('close', (code) => this.close(code)); this.nested.source.once('upgrade', (request) => this.nested.headers = request.headers); this.nested.source.on('message', (payload) => this.observable.next(message_1.RequestMessage.build('outgoing', payload))); this.nested.source.on('error', (error) => { if (!webSocketClosingStates.includes(this.nested.source.readyState)) { this.observable.error(errors_1.WebSocketConnectionError.build(this, error)); this.close(); } }); if (this.configuration.signal) { this.configuration.signal.aborted ? this.handleAbortSignal() : this.configuration.signal.addEventListener('abort', () => this.handleAbortSignal()); } this.scheduleConnectionClosing(); return this; } catch (error) { this.nested.error = errors_1.WebSocketConnectionError.build(this, error); this.observable.error(this.nested.error); return this; } } close(status) { if (typeof status === 'number' && this.nested.status === null) { this.nested.status = status; } this.nested.source?.close(); setImmediate(() => this.observable.complete()); } /** Resolves `connecting` ready state of the web socket */ async resolve() { if (!this.nested.source) { throw errors_1.WebSocketConnectionError.build(this, this.nested.error ?? 'Connection was not estabilished'); } if (this.configuration.signal?.aborted) { return null; } if (this.nested.source.readyState === this.nested.source.CONNECTING) { await Promise.race([ new Promise((resolve) => this.nested.source.once('open', resolve)), new Promise((resolve) => this.nested.source.once('close', resolve)), new Promise((resolve, reject) => this.nested.source.once('error', reject)), new Promise((resolve) => this.configuration.signal?.addEventListener('abort', resolve)), ]).catch((error) => { logger.error('Got error while resolving connection', error?.stack ?? error); throw errors_1.WebSocketConnectionError.build(this, error); }); } } handleAbortSignal() { if (!this.nested.source) { throw errors_1.WebSocketConnectionError.build(this, this.nested.error ?? 'Connection was not estabilished'); } if (!webSocketClosingStates.includes(this.nested.source.readyState)) { logger.warn(`Outgoing request [WS ${this.url}] has aborted using abort controller`, `[${this.configuration.signal?.reason}]`); } this.close(); } async scheduleConnectionClosing() { if (!this.nested.source) { throw errors_1.WebSocketConnectionError.build(this, this.nested.error ?? 'Connection was not estabilished'); } if (!this.configuration.timeout) { return null; } if (!webSocketClosingStates.includes(this.nested.source.readyState)) { const timer = (0, utils_1.wait)(this.configuration.timeout); await Promise.race([timer, new Promise((resolve) => this.nested.source.once('close', resolve))]).then(() => timer.abort()); } if (!webSocketClosingStates.includes(this.nested.source.readyState)) { logger.info(`Connection has closed by timeout [${this.configuration.timeout}]`); } this.close(); } static build(configuration) { const query = configuration.query ? `?${qs_1.default.stringify(configuration.query)}` : ''; const url = configuration.url ? configuration.baseURL ? `${configuration.baseURL.replace(/\/$/, '')}/${configuration.url.replace(/^\//, '')}${query}` : `${configuration.url}${query}` : configuration.baseURL ? `${configuration.baseURL}${query}` : null; if (!url) { throw new Error('[url] is invalid'); } return new WebSocket(url, configuration); } } exports.WebSocket = WebSocket; //# sourceMappingURL=model.js.map