Javascript – Node.JS Code Improvement (protobuf, tls)

I'm working with an environment that allows me to execute node.js code from a single file without installing additional modules, My job is to write a simple SSL socket client that sends protobuf messages, receives and decodes responses in the same format.

What I've developed in this code sample:

  1. Protobuf decoder / encoder (I have implemented only types that I will use);
  2. SSL Socket class;

I'm a newbie in node.js (6 hours experience: D) so you need to show me weak parts of this code and share best practices with me:

const tls = require("tls");

const HOST = "example.com";
const PORT = 12345;

const ProtoTypes = Object.freeze({"Varint": 0, "_64bit": 1, "LengthDelimited": 2, "_32bit": 5});

class ProtoField {
    constructor (type, tag, value=null) {
        if (isNaN(tag) || tag < 0)
            throw new Error("Invalid tag");

        this.type = type;
        this.tag = tag;
        this.value = value;
    }

    encodePrefix() {
        return VarintField.encode((this.tag << 3) | this.type);
    }

    static decodePrefix(stream, position=0) {
        var decoded_prefix = VarintField.decode(stream, position);
        return {
            type: decoded_prefix.value & 7,
            tag: decoded_prefix.value >> 3,
            length: decoded_prefix.length
        }
    }

    encode() {
        var prefix = this.encodePrefix();

        return Buffer.concat((prefix, this.constructor.encode(this.value)));
    }

    decode(stream, position=0) {
        var decoded = this.constructor.decode(stream, position);
        this.value = decoded.value;

        return decoded.length;
    }
}

class VarintField extends ProtoField {
    constructor(tag, value=null) {
        if (value != null && isNaN(value))
            throw new Error("Invalid value");

        super(ProtoTypes.Varint, tag, value);
    }

    static encode(value) {
        var number = value, buf = ();

        for (;;) {
            var toWrite = number & 0x7F;
            number >>= 7;
            if (number != 0)
                buf.push(toWrite | 0x80);
            else {
                buf.push(toWrite);
                break;
            }
        }

        return Buffer.from(buf);
    }

    static decode(stream, position=0) {
        var number =  0, bytes_read = 0;

        for (var shift = 0; position + bytes_read < stream.length; bytes_read++, shift += 7) {
            var b = stream(position + bytes_read);
            number |= (b & 0x7f) << shift;
            if ((b & 0x80) == 0) {
                bytes_read += 1;
                break;
            }
        }

        return {
            value: number,
            length: bytes_read
        }
    }
}

class BytesField extends ProtoField {
    constructor(tag, value=null) {
        if (value != null && !Buffer.isBuffer(value))
            throw new Error("Invalid value");

        super(ProtoTypes.LengthDelimited, tag, value);
    }

    static encode(value) {
        return Buffer.concat((VarintField.encode(value.length), Buffer.from(value)));
    }

    static decode(stream, position=0) {
        var length = VarintField.decode(stream, position);
        return {
            value: stream.slice(position + length.length, position + length.length + length.value),
            length: length.length + length.value
        }
    }
}

class StringField extends ProtoField {
    constructor(tag, value=null) {
        if (value != null && String(value) !== value)
            throw new Error("Invalid value");

        super(ProtoTypes.LengthDelimited, tag, value);
    }

    static encode(value) {
        return Buffer.concat((VarintField.encode(value.length), Buffer.from(value, "utf-8")));
    }

    static decode(stream, position=0) {
        var length = VarintField.decode(stream, position);
        return {
            value: stream.slice(position + length.length, position + length.length + length.value).toString("utf-8"),
            length: length.length + length.value
        }
    }
}

class _32bitField {
    static decode(stream, position=0) {
        return {value: null, length: 4};
    }
}

class _64bitField {
    static decode(stream, position=0) {
        return {value: null, length: 8};
    }
}

const ProtoTypesDefaultClasses = Object.freeze({
    (ProtoTypes.Varint): VarintField,
    (ProtoTypes.LengthDelimited): BytesField,
    (ProtoTypes._32bit): _32bitField,
    (ProtoTypes._64bit): _64bitField
});

class Message {
    constructor() {
        this.fields = ();
    }

    addField(field) {
        this.fields(field.tag) = field;
        return field;
    }

    encode() {
        var buffers = ();
        this.fields.forEach(function(field) {
            if (field.value != null)
                buffers.push(field.encode());
        });
        return Buffer.concat(buffers);
    }

    decode(stream, position=0, length=0) {
        var bytes_read = 0, length = length > 0 ? length : stream.length - position;

        for (; bytes_read < length;) {
            var prefix = ProtoField.decodePrefix(stream, position + bytes_read);
            if (this.fields(prefix.tag) != undefined)
                bytes_read += this.fields(prefix.tag).decode(stream, position + bytes_read + prefix.length) + prefix.length;
            else
                bytes_read += ProtoTypesDefaultClasses(prefix.type).decode(stream, position + bytes_read + prefix.length).length + prefix.length;
        }

        return bytes_read;
    } 
}

class EmbeddedMessageField extends ProtoField {
    constructor(tag, value) {
        if (!(value instanceof Message))
            throw new Error("Invalid value");

        super(ProtoTypes.LengthDelimited, tag, value);
    }

    static encode(value) {
        return BytesField.encode(value.encode());
    }

    decode(stream, position=0) {
        var length = VarintField.decode(stream, position);

        return length.length + this.value.decode(stream, position + 1, length.value)
    }
}

class ExampleEmbededMessage extends Message {
    constructor(f1, f2, f3) {
        super();

        this.e_field1 = this.addField(new BytesField(1, f1));
        this.e_field2 = this.addField(new VarintField(2, f2));
        this.e_field3 = this.addField(new StringField(4, f3));
    }
}

class ExampleRequestMessage extends Message {
    constructor(f0, f1, f2, f3) {
        super();

        this.field1 = this.addField(new StringField(1, f0));
        this.embedded = this.addField(new EmbeddedMessageField(2, new ExampleEmbededMessage(f1, f2, f3)));
        this.constant_field = this.addField(new StringField(3, "constant"));
    }
}

class ExampleResponseMessage extends Message {
    constructor() {
        super();

        this.resp_field1 = this.addField(new StringField(1));
        this.resp_field2 = this.addField(new VarintField(2));
        this.resp_field15 = this.addField(new StringField(15));
    }
}

class Packet {
    constructor(id, message) {
        this.id = id;
        this.message = message;
    }

    encode() {
        return Buffer.concat((Buffer.from((this.id)), this.message.encode()));
    }

    decode(stream) {
        if (stream.length == 0)
            return 0;
        this.id = stream(0);
        return this.message.decode(stream, 1) + 1;
    }
}

class TLSClient {
    constructor(host, port) {
        this.host = host;
        this.port = port;
        this.options = {rejectUnauthorized: false};
        this.init()
    }

    init() {
        var client = this;
        this.socket = tls.connect(client.port, client.host, client.options);
    }

    send(packet) {
        var client = this;
        return new Promise((resolve, reject) => {
            var encoded_packet = packet.encode();
            var length = Buffer.alloc(4);
            length.writeUInt32LE(encoded_packet.length);

            client.socket.write(Buffer.concat((length, encoded_packet)));

            client.socket.on("data", (data) => {
                resolve(data);
                client.socket.destroy();
            });
            client.socket.on("error", (err) => {
                reject(err);
            });
        });
    }
}

var f0 = "string1", f1 = Buffer.from((0, 1, 2, 3, 4, 5)), f2 = 54321, f3 = "string2";
var request_packet = new Packet(13, new ExampleRequestMessage(f0, f1, f2, f3));
var response_packet = new Packet(13, new ExampleResponseMessage());

var sock = new TLSClient(HOST, PORT);

sock.send(request_packet)
.then((data) => {
    response_packet.decode(data, 4);
    if (response_packet.resp_field2.value > 0)
        console.log("Great!");
    else
        console.log("Zero.");
})
.catch((err) => {
    console.log(err);
});

P.S. I know it's 300 lines of code, I've tried to cut as much as possible ..