UNPKG

mockttp

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

264 lines 9.92 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientServerChannel = exports.Serializable = void 0; exports.serialize = serialize; exports.deserialize = deserialize; exports.serializeBuffer = serializeBuffer; exports.deserializeBuffer = deserializeBuffer; exports.ensureParamsDeferenced = ensureParamsDeferenced; exports.serializeProxyConfig = serializeProxyConfig; exports.deserializeProxyConfig = deserializeProxyConfig; const buffer_1 = require("buffer"); const stream_1 = require("stream"); const _ = require("lodash"); const rule_parameters_1 = require("../rules/rule-parameters"); function serialize(obj, stream) { const channel = new ClientServerChannel(stream); const data = obj.serialize(channel); data.topicId = channel.topicId; return data; } function deserialize(data, stream, options, lookup) { const type = data.type; const channel = new ClientServerChannel(stream, data.topicId); const deserialized = lookup[type].deserialize(data, channel, options); // Wrap .dispose and ensure the channel is always disposed too. const builtinDispose = deserialized.dispose; deserialized.dispose = () => { builtinDispose(); channel.dispose(); }; return deserialized; } class Serializable { /** * @internal */ serialize(_channel) { // By default, we assume data is transferrable as-is return this; } /** * @internal */ static deserialize(data, _channel, _options // Varies, e.g. in plugins. ) { // By default, we assume we just need to assign the right prototype return _.create(this.prototype, data); } // This rule is being unregistered. Any handlers who need to cleanup when they know // they're no longer in use should implement this and dispose accordingly. // Only deserialized rules are disposed - if the originating rule needs // disposing too, ping the channel and let it know. dispose() { } } exports.Serializable = Serializable; const DISPOSE_MESSAGE = { disposeChannel: true }; // Wraps another stream, ensuring that messages go only to the paired channel on the // other client/server. In practice, each handler gets one end of these streams in // their serialize/deserialize methods, and can use them to sync live data reliably. class ClientServerChannel extends stream_1.Duplex { constructor(rawStream, topicId) { super({ objectMode: true }); this.rawStream = rawStream; this._onRawStreamError = (error) => { this.destroy(error); }; this._onRawStreamFinish = () => { this.end(); }; this._readFromRawStream = (rawData) => { const stringData = rawData.toString(); stringData.split('\n').filter(d => !!d).forEach((rawDataLine) => { let data; try { data = JSON.parse(rawDataLine); } catch (e) { console.log(e); console.log('Received unparseable message, dropping.', rawDataLine.toString()); return; } if (data.topicId === this.topicId) { if (_.isEqual(_.omit(data, 'topicId'), DISPOSE_MESSAGE)) this.dispose(true); else this.push(data); } }); }; this.reading = false; this.topicId = topicId || crypto.randomUUID(); this.rawStream.on('error', this._onRawStreamError); this.rawStream.on('finish', this._onRawStreamFinish); } /** * @internal @hidden */ _write(message, encoding, callback) { message.topicId = this.topicId; const chunk = JSON.stringify(message) + '\n'; if (!this.rawStream.write(chunk, encoding)) { this.rawStream.once('drain', callback); } else { callback(); } } _read() { if (!this.reading) { this.rawStream.on('data', this._readFromRawStream); this.reading = true; } } request(actionOrData, dataOrNothing) { let action; let data; if (_.isString(actionOrData)) { action = actionOrData; data = dataOrNothing; } else { data = actionOrData; } const requestId = crypto.randomUUID(); return new Promise((resolve, reject) => { const responseListener = (response) => { if (response.requestId === requestId) { if (response.error) { // Derialize error from plain object reject(Object.assign(new Error(), { stack: undefined }, response.error)); } else { resolve(response.data); } this.removeListener('data', responseListener); } }; const request = { data, requestId }; if (action) request.action = action; this.write(request, (e) => { if (e) reject(e); else this.on('data', responseListener); }); }); } onRequest(cbOrAction, cbOrNothing) { let actionName; let cb; if (_.isString(cbOrAction)) { actionName = cbOrAction; cb = cbOrNothing; } else { cb = cbOrAction; } this.on('data', async (request) => { const { requestId, action } = request; // Filter by actionName, if set if (actionName !== undefined && action !== actionName) return; try { const response = { requestId, data: await cb(request.data) }; if (!this.writable) return; // Response too slow - drop it this.write(response); } catch (error) { // Make the error serializable: error = _.pick(error, Object.getOwnPropertyNames(error)); if (!this.writable) return; // Response too slow - drop it this.write({ requestId, error }); } }); } // Shuts down the channel. Only needs to be called on one side, the other side // will be shut down automatically when it receives DISPOSE_MESSAGE. dispose(disposeReceived = false) { this.on('error', () => { }); // Dispose is best effort - we don't care about errors // Only one side needs to send a dispose - we send first if we haven't seen one. if (!disposeReceived) this.end(DISPOSE_MESSAGE); else this.end(); // Detach any remaining onRequest handlers: this.removeAllListeners('data'); // Stop receiving upstream messages from the global stream: this.rawStream.removeListener('data', this._readFromRawStream); this.rawStream.removeListener('error', this._onRawStreamError); this.rawStream.removeListener('finish', this._onRawStreamFinish); } } exports.ClientServerChannel = ClientServerChannel; function serializeBuffer(buffer) { return buffer.toString('base64'); } function deserializeBuffer(buffer) { return buffer_1.Buffer.from(buffer, 'base64'); } const SERIALIZED_PARAM_REFERENCE = "__mockttp__param__reference__"; function serializeParam(value) { // Swap the symbol for a string, since we can't serialize symbols in JSON: return { [SERIALIZED_PARAM_REFERENCE]: value[rule_parameters_1.MOCKTTP_PARAM_REF] }; } function isSerializedRuleParam(value) { return value && SERIALIZED_PARAM_REFERENCE in value; } function ensureParamsDeferenced(value, ruleParams) { if (isSerializedRuleParam(value)) { const paramRef = { [rule_parameters_1.MOCKTTP_PARAM_REF]: value[SERIALIZED_PARAM_REFERENCE] }; return (0, rule_parameters_1.dereferenceParam)(paramRef, ruleParams); } else { return value; } } function serializeProxyConfig(proxyConfig, channel) { if (_.isFunction(proxyConfig)) { const callbackId = `proxyConfig-callback-${crypto.randomUUID()}`; channel.onRequest(callbackId, proxyConfig); return callbackId; } else if (_.isArray(proxyConfig)) { return proxyConfig.map((config) => serializeProxyConfig(config, channel)); } else if ((0, rule_parameters_1.isParamReference)(proxyConfig)) { return serializeParam(proxyConfig); } else if (proxyConfig) { return { ...proxyConfig, trustedCAs: proxyConfig.trustedCAs?.map((caDefinition) => 'cert' in caDefinition ? { cert: caDefinition.cert.toString('utf8') } // Stringify in case of buffers : caDefinition), additionalTrustedCAs: proxyConfig.additionalTrustedCAs?.map((caDefinition) => 'cert' in caDefinition ? { cert: caDefinition.cert.toString('utf8') } // Stringify in case of buffers : caDefinition) }; } } function deserializeProxyConfig(proxyConfig, channel, ruleParams) { if (_.isString(proxyConfig)) { const callbackId = proxyConfig; const proxyConfigCallback = async (options) => { return await channel.request(callbackId, options); }; return proxyConfigCallback; } else if (_.isArray(proxyConfig)) { return proxyConfig.map((config) => deserializeProxyConfig(config, channel, ruleParams)); } else { return ensureParamsDeferenced(proxyConfig, ruleParams); } } //# sourceMappingURL=serialization.js.map