@n1k1t/mock-server
Version:
The ultimate toolkit to intercept, transform, and simulate HTTP/WS traffic with type-safe expectations
177 lines • 7.37 kB
JavaScript
"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