"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.deserializeProxyConfig = exports.serializeProxyConfig = exports.ensureParamsDeferenced = exports.maybeSerializeParam = exports.deserializeBuffer = exports.serializeBuffer = exports.ClientServerChannel = exports.Serializable = exports.deserialize = exports.serialize = void 0;
const _ = require("lodash");
const stream_1 = require("stream");
const uuid_1 = require("uuid");
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;
}
exports.serialize = serialize;
function deserialize(data, stream, ruleParams, lookup) {
    const type = data.type;
    const channel = new ClientServerChannel(stream, data.topicId);
    const deserialized = lookup[type].deserialize(data, channel, ruleParams);
    // Wrap .dispose and ensure the channel is always disposed too.
    const builtinDispose = deserialized.dispose;
    deserialized.dispose = () => {
        builtinDispose();
        channel.dispose();
    };
    return deserialized;
}
exports.deserialize = deserialize;
class Serializable {
    /**
     * @internal
     */
    serialize(_channel) {
        // By default, we assume data is transferrable as-is
        return this;
    }
    /**
     * @internal
     */
    static deserialize(data, _channel, _ruleParams) {
        // 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 || (0, uuid_1.v4)();
        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 = (0, uuid_1.v4)();
        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');
}
exports.serializeBuffer = serializeBuffer;
function deserializeBuffer(buffer) {
    return Buffer.from(buffer, 'base64');
}
exports.deserializeBuffer = deserializeBuffer;
const SERIALIZED_PARAM_REFERENCE = "__mockttp__param__reference__";
function maybeSerializeParam(value) {
    if ((0, rule_parameters_1.isParamReference)(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] };
    }
    else {
        return value;
    }
}
exports.maybeSerializeParam = maybeSerializeParam;
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;
    }
}
exports.ensureParamsDeferenced = ensureParamsDeferenced;
function serializeProxyConfig(proxyConfig, channel) {
    if (_.isFunction(proxyConfig)) {
        const callbackId = `proxyConfig-callback-${(0, uuid_1.v4)()}`;
        channel.onRequest(callbackId, proxyConfig);
        return callbackId;
    }
    else if (_.isArray(proxyConfig)) {
        return proxyConfig.map((config) => serializeProxyConfig(config, channel));
    }
    else {
        return maybeSerializeParam(proxyConfig);
    }
}
exports.serializeProxyConfig = serializeProxyConfig;
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);
    }
}
exports.deserializeProxyConfig = deserializeProxyConfig;
//# sourceMappingURL=serialization.js.map