<commit_before>'use strict';

var inherits = require('inherits'); // Used instead of full `util` package for the sake of easier browserification
var EventEmitter = require('events').EventEmitter;
var Nested = require('nested-observe');
var objectPath = require('object-path');
var replicator = require('./replicator');
var equal = require('deep-equal');
var clone = require('clone');
var uuid = require('uuid');

var inBrowser = typeof document !== 'undefined';
var declaredReplicants = {};

// Make Replicant inherit the prototype of EventEmitter
inherits(Replicant, EventEmitter);
module.exports = Replicant;

function Replicant(name, bundle, opts, nodecg) {
    if (!name || typeof name !== 'string') {
        throw new Error('Must supply a name when instantiating a Replicant');
    }

    if (!nodecg || typeof nodecg !== 'object') {
        throw new Error('Must supply a nodecg API context when instantiating a Replicant');
    }

    // Load logger
    var log = require('./logger')('Replicant/' + bundle + '.' + name);

    // If replicant already exists, return that.
    if (declaredReplicants.hasOwnProperty(bundle)) {
        if (declaredReplicants[bundle].hasOwnProperty(name)) {
            log.warn('Attempted to re-declare replicant "%s" in bundle "%s", returning existing replicant instead',
                name, bundle);
            return declaredReplicants[bundle][name];
        } else {
            declaredReplicants[bundle][name] = this;
        }
    } else {
        declaredReplicants[bundle] = {};
        declaredReplicants[bundle][name] = this;
    }

    var self = this;
    var value;
    opts = opts || {};
    this.name = name;
    this.bundle = bundle || nodecg.bundleName;
    this.opts = opts;
    this.revision = 0;
    this.status = 'undeclared';
    this.id = uuid.v4();
    if (typeof opts.persistent === 'undefined') opts.persistent = true;

    // When running in the browser, we have to wait until the socket joins the room
    // and the replicant is fully declared before running any additional commands.
    // After this time, commands do not need to be added to the queue and are simply executed immediately.
    var queue = [];

    function addToQueue(fn, args) {
        queue.push({fn: fn, args: args});
    }

    function processQueue() {
        queue.forEach(function(item) {
            item.fn.apply(this, item.args);
        });
    }

    assignValue(opts.defaultValue);

    Object.defineProperty(this, 'value', {
        get: function() {
            return value;
        },
        set: function(newValue) {
            if (newValue === value) {
                log.replicants('value unchanged, no action will be taken');
                return value;
            }

            log.replicants('running setter with', newValue);

            if (self.status !== 'declared') {
                addToQueue(doBrowserSetter, [newValue]);
                return;
            }

            if (inBrowser) {
                doBrowserSetter(newValue);
            } else {
                replicator.assign(name, bundle, newValue);
            }
        },
        enumerable: true
    });

    function doBrowserSetter(newValue) {
        // Run the assignment and emit the change event immediately,
        // with the assumption that the dispatched assignment will be accepted.
        assignValue(newValue);

        nodecg.socket.emit('assignReplicant', {
            name: name,
            bundle: bundle,
            value: newValue
        });
    }

    function doBrowserDeclare(defaultValue) {
        if (self.status === 'declared' || self.status === 'declaring') return;
        self.status = 'declaring';
        nodecg.socket.emit('joinRoom', bundle, function() {
            nodecg.socket.emit('declareReplicant', {
                name: name,
                bundle: bundle,
                defaultValue: defaultValue,
                persistent: opts.persistent
            }, function(data) {
                log.replicants('declareReplicant callback (value: %s, revision: %s)', data.value, data.revision);
                if (self.revision !== data.revision || !equal(value, data.value)) {
                    unobserveValue();
                    value = defaultValue;
                    observeValue();
                    assignValue(data.value, data.revision);
                }
                self.status = 'declared';
                self.emit('declared', data);
                if (queue.length) {
                    processQueue();
                } else {
                    self.emit('change', undefined, data.value);
                }
            });
        });
    }

    if (inBrowser) {
        doBrowserDeclare(opts.defaultValue);
    } else {
        (function() {
            var result = replicator.findOrDeclare(name, bundle, opts.defaultValue, opts.persistent);
            assignValue(result.value, result.revision);
            self.status = 'declared';
            processQueue();
        })();
    }

    // Overwrites the value completely, and assigns a new one.
    // Does not send a `change` argument with the change event.
    function assignValue(newValue, revision) {
        unobserveValue();
        var oldValue = clone(value);
        value = newValue;
        observeValue();
        if (revision) {
            self.revision = revision;
        }
        self.emit('change', oldValue, value);
    }

    function observeValue() {
        if (typeof value === 'object') {
            Nested.observe(value, onValueChange);
        }
    }

    function replayChange(changes) {
        try {
            unobserveValue();
            var oldValue = clone(value);
            changes.forEach(function(change) {
                switch(change.type) {
                    case 'add':
                    case 'update':
                        objectPath.set(value, change.path, change.newValue);
                        break;
                    case 'splice':
                        var arr = objectPath.get(value, change.path);
                        var args = clone(change.added);
                        args.unshift(change.removedCount);
                        args.unshift(change.index);
                        Array.prototype.splice.apply(arr, args);
                        objectPath.set(value, change.path, arr);
                        break;
                    case 'delete':
                        objectPath.del(value, change.path);
                        break;
                }
            });
            observeValue();
            dispatchChanges(oldValue, changes);
        } catch (e) {
            log.error(e.stack);
        }
    }

    function dispatchChanges(changes) {
        if (inBrowser) {
            nodecg.socket.emit('changeReplicant', {
                name: name,
                bundle: bundle,
                changes: changes,
                revision: self.revision,
                originatorId: self.id
            }, function(value, revision) {
                // This callback is only invoked in cases of failure
                log.replicants('Not at head revision (ours %s, theirs %s). Change aborted & head revision applied.',
                    self.revision, revision);
                assignValue(value, revision);
            });
        } else {
            replicator.change(name, bundle, changes, self.id);
        }
    }

    function onValueChange(rawChanges) {
        var formattedChanges = [];
        rawChanges.forEach(function(change) {
            var path = change.path.substr(1).replace(/\//g,'.');
            var newVal = objectPath.get(change.root, path);
            switch(change.type) {
                case 'add':
                    formattedChanges.push({
                        type: 'add',
                        path: path,
                        newValue: newVal
                    });
                    break;
                case 'update':
                    formattedChanges.push({
                        type: 'update',
                        path: path,
                        oldValue: change.oldValue,
                        newValue: newVal
                    });
                    break;
                case 'splice':
                    formattedChanges.push({
                        type: 'splice',
                        path: path,
                        index: change.index,
                        removed: change.removed,
                        removedCount: change.removed.length,
                        added: change.object.slice(change.index, change.index+change.addedCount),
                        addedCount: change.addedCount
                    });
                    break;
                case 'delete':
                    formattedChanges.push({
                        type: 'delete',
                        path: path,
                        oldValue: change.oldValue
                    });
                    break;
                default:
                    formattedChanges.push({
                        type: 'other',
                        path: path
                    });
            }
        });

        if (self.status === 'declared') {
            dispatchChanges(formattedChanges);
        } else {
            addToQueue(dispatchChanges, [formattedChanges]);
        }
    }

    function unobserveValue() {
        if (typeof value === 'object') {
            Nested.unobserve(value, onValueChange);
        }
    }

    // Handle assignment that overwrites the whole value
    if (inBrowser) {
        nodecg.socket.on('replicantAssigned', replicantAssigned);
    } else {
        replicator.on('replicantAssigned', replicantAssigned);
    }

    function replicantAssigned(data) {
        if (data.name !== name || data.bundle !== bundle) return;
        log.replicants('replicantAssigned', data);<commit_msg>[replicants] `null` is an object? seriously?!<commit_after>'use strict';

var inherits = require('inherits'); // Used instead of full `util` package for the sake of easier browserification
var EventEmitter = require('events').EventEmitter;
var Nested = require('nested-observe');
var objectPath = require('object-path');
var replicator = require('./replicator');
var equal = require('deep-equal');
var clone = require('clone');
var uuid = require('uuid');

var inBrowser = typeof document !== 'undefined';
var declaredReplicants = {};

// Make Replicant inherit the prototype of EventEmitter
inherits(Replicant, EventEmitter);
module.exports = Replicant;

function Replicant(name, bundle, opts, nodecg) {
    if (!name || typeof name !== 'string') {
        throw new Error('Must supply a name when instantiating a Replicant');
    }

    if (!nodecg || typeof nodecg !== 'object') {
        throw new Error('Must supply a nodecg API context when instantiating a Replicant');
    }

    // Load logger
    var log = require('./logger')('Replicant/' + bundle + '.' + name);

    // If replicant already exists, return that.
    if (declaredReplicants.hasOwnProperty(bundle)) {
        if (declaredReplicants[bundle].hasOwnProperty(name)) {
            log.warn('Attempted to re-declare replicant "%s" in bundle "%s", returning existing replicant instead',
                name, bundle);
            return declaredReplicants[bundle][name];
        } else {
            declaredReplicants[bundle][name] = this;
        }
    } else {
        declaredReplicants[bundle] = {};
        declaredReplicants[bundle][name] = this;
    }

    var self = this;
    var value;
    opts = opts || {};
    this.name = name;
    this.bundle = bundle || nodecg.bundleName;
    this.opts = opts;
    this.revision = 0;
    this.status = 'undeclared';
    this.id = uuid.v4();
    if (typeof opts.persistent === 'undefined') opts.persistent = true;

    // When running in the browser, we have to wait until the socket joins the room
    // and the replicant is fully declared before running any additional commands.
    // After this time, commands do not need to be added to the queue and are simply executed immediately.
    var queue = [];

    function addToQueue(fn, args) {
        queue.push({fn: fn, args: args});
    }

    function processQueue() {
        queue.forEach(function(item) {
            item.fn.apply(this, item.args);
        });
    }

    assignValue(opts.defaultValue);

    Object.defineProperty(this, 'value', {
        get: function() {
            return value;
        },
        set: function(newValue) {
            if (newValue === value) {
                log.replicants('value unchanged, no action will be taken');
                return value;
            }

            log.replicants('running setter with', newValue);

            if (self.status !== 'declared') {
                addToQueue(doBrowserSetter, [newValue]);
                return;
            }

            if (inBrowser) {
                doBrowserSetter(newValue);
            } else {
                replicator.assign(name, bundle, newValue);
            }
        },
        enumerable: true
    });

    function doBrowserSetter(newValue) {
        // Run the assignment and emit the change event immediately,
        // with the assumption that the dispatched assignment will be accepted.
        assignValue(newValue);

        nodecg.socket.emit('assignReplicant', {
            name: name,
            bundle: bundle,
            value: newValue
        });
    }

    function doBrowserDeclare(defaultValue) {
        if (self.status === 'declared' || self.status === 'declaring') return;
        self.status = 'declaring';
        nodecg.socket.emit('joinRoom', bundle, function() {
            nodecg.socket.emit('declareReplicant', {
                name: name,
                bundle: bundle,
                defaultValue: defaultValue,
                persistent: opts.persistent
            }, function(data) {
                log.replicants('declareReplicant callback (value: %s, revision: %s)', data.value, data.revision);
                if (self.revision !== data.revision || !equal(value, data.value)) {
                    unobserveValue();
                    value = defaultValue;
                    observeValue();
                    assignValue(data.value, data.revision);
                }
                self.status = 'declared';
                self.emit('declared', data);
                if (queue.length) {
                    processQueue();
                } else {
                    self.emit('change', undefined, data.value);
                }
            });
        });
    }

    if (inBrowser) {
        doBrowserDeclare(opts.defaultValue);
    } else {
        (function() {
            var result = replicator.findOrDeclare(name, bundle, opts.defaultValue, opts.persistent);
            assignValue(result.value, result.revision);
            self.status = 'declared';
            processQueue();
        })();
    }

    // Overwrites the value completely, and assigns a new one.
    // Does not send a `change` argument with the change event.
    function assignValue(newValue, revision) {
        unobserveValue();
        var oldValue = clone(value);
        value = newValue;
        observeValue();
        if (revision) {
            self.revision = revision;
        }
        self.emit('change', oldValue, value);
    }

    function observeValue() {
        if (typeof value === 'object' && value !== null) {
            Nested.observe(value, onValueChange);
        }
    }

    function replayChange(changes) {
        try {
            unobserveValue();
            var oldValue = clone(value);
            changes.forEach(function(change) {
                switch(change.type) {
                    case 'add':
                    case 'update':
                        objectPath.set(value, change.path, change.newValue);
                        break;
                    case 'splice':
                        var arr = objectPath.get(value, change.path);
                        var args = clone(change.added);
                        args.unshift(change.removedCount);
                        args.unshift(change.index);
                        Array.prototype.splice.apply(arr, args);
                        objectPath.set(value, change.path, arr);
                        break;
                    case 'delete':
                        objectPath.del(value, change.path);
                        break;
                }
            });
            observeValue();
            dispatchChanges(oldValue, changes);
        } catch (e) {
            log.error(e.stack);
        }
    }

    function dispatchChanges(changes) {
        if (inBrowser) {
            nodecg.socket.emit('changeReplicant', {
                name: name,
                bundle: bundle,
                changes: changes,
                revision: self.revision,
                originatorId: self.id
            }, function(value, revision) {
                // This callback is only invoked in cases of failure
                log.replicants('Not at head revision (ours %s, theirs %s). Change aborted & head revision applied.',
                    self.revision, revision);
                assignValue(value, revision);
            });
        } else {
            replicator.change(name, bundle, changes, self.id);
        }
    }

    function onValueChange(rawChanges) {
        var formattedChanges = [];
        rawChanges.forEach(function(change) {
            var path = change.path.substr(1).replace(/\//g,'.');
            var newVal = objectPath.get(change.root, path);
            switch(change.type) {
                case 'add':
                    formattedChanges.push({
                        type: 'add',
                        path: path,
                        newValue: newVal
                    });
                    break;
                case 'update':
                    formattedChanges.push({
                        type: 'update',
                        path: path,
                        oldValue: change.oldValue,
                        newValue: newVal
                    });
                    break;
                case 'splice':
                    formattedChanges.push({
                        type: 'splice',
                        path: path,
                        index: change.index,
                        removed: change.removed,
                        removedCount: change.removed.length,
                        added: change.object.slice(change.index, change.index+change.addedCount),
                        addedCount: change.addedCount
                    });
                    break;
                case 'delete':
                    formattedChanges.push({
                        type: 'delete',
                        path: path,
                        oldValue: change.oldValue
                    });
                    break;
                default:
                    formattedChanges.push({
                        type: 'other',
                        path: path
                    });
            }
        });

        if (self.status === 'declared') {
            dispatchChanges(formattedChanges);
        } else {
            addToQueue(dispatchChanges, [formattedChanges]);
        }
    }

    function unobserveValue() {
        if (typeof value === 'object' && value !== null) {
            Nested.unobserve(value, onValueChange);
        }
    }

    // Handle assignment that overwrites the whole value
    if (inBrowser) {
        nodecg.socket.on('replicantAssigned', replicantAssigned);
    } else {
        replicator.on('replicantAssigned', replicantAssigned);
    }

    function replicantAssigned(data) {
        if (data.name !== name || data.bundle !== bundle) return;
        log.replicants('replicantAssigned', data);
{'fraction_non_alphanumeric': 0.10802780016116036, 'fraction_numerical': 0.00030217566478646255, 'mean_word_length': 1.5218440436880873, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7439175', 'n_tokens_mistral': 5090, 'n_tokens_neox': 4850, 'n_words': 1210}