<commit_before>define([
    'json.sortify',
    '/common/common-util.js',
    '/common/hyperscript.js',
    '/common/common-interface.js',
    '/customize/messages.js'
], function (Sortify, Util, h, UI, Messages) {
    var Comments = {};

/*
{
    authors: {
        "id": {
            name: "",
            curvePublic: "",
            avatar: "",
            profile: ""
        }
    },
    data: {
        "uid": {
            m: [{
                u: id,
                m: "str", // comment
                t: +new Date,
                v: "str" // value of the commented content
            }],
            (deleted: undefined/true,)
        }
    }
}
*/

    var COMMENTS = {
        authors: {},
        data: {}
    };

    var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };

    // XXX function duplicated from www/code/markers.js
    var authorUid = function (existing) {
        if (!Array.isArray(existing)) { existing = []; }
        var n;
        var i = 0;
        while (!n || existing.indexOf(n) !== -1 && i++ < 1000) {
            n = Math.floor(Math.random() * 1000000);
        }
        // If we can't find a valid number in 1000 iterations, use 0...
        if (existing.indexOf(n) !== -1) { n = 0; }
        return n;
    };
    var getAuthorId = function (Env, curve) {
        var existing = Object.keys(Env.comments.authors || {}).map(Number);
        if (!Env.common.isLoggedIn()) { return authorUid(existing); }

        var uid;
        existing.some(function (id) {
            var author = Env.comments.authors[id] || {};
            if (author.curvePublic !== curve) { return; }
            uid = Number(id);
            return true;
        });
        return uid || authorUid(existing);
    };

    var updateAuthorData = function (Env) {
        var userData = Env.metadataMgr.getUserData();
        var myAuthorId = getAuthorId(Env, userData.curvePublic);
        var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {};
        data.name = userData.name;
        data.avatar = userData.avatar;
        data.profile = userData.profile;
        data.curvePublic = userData.curvePublic;
        console.log(data);
        return myAuthorId;
    };

    var updateMetadata = function (Env) {
        var md = Util.clone(Env.metadataMgr.getMetadata());
        md.comments = Util.clone(Env.comments);
        Env.metadataMgr.updateMetadata(md);
    };

    Messages.comments_submit = "Submit"; // XXX
    Messages.comments_reply = "Reply"; // XXX
    Messages.comments_resolve = "Resolve"; // XXX

    var getCommentForm = function (Env, reply, _cb) {
        var cb = Util.once(_cb);
        var userData = Env.metadataMgr.getUserData();
        var name = Util.fixHTML(userData.name || Messages.anonymous);
        var avatar = h('span.cp-avatar');
        var textarea = h('textarea');
        Env.common.displayAvatar($(avatar), userData.avatar, name);

        var cancel = h('button.btn.btn-cancel', [
            h('i.fa.fa-times'),
            Messages.cancel
        ]);
        var submit = h('button.btn.btn-primary', [
            h('i.fa.fa-paper-plane-o'),
            Messages.comments_submit
        ]);

        var done = false;

        $(submit).click(function (e) {
            e.stopPropagation();
            cb(textarea.value);
        });
        $(cancel).click(function (e) {
            e.stopPropagation();
            cb();
        });

        $(textarea).keydown(function (e) {
            if (e.which === 27) {
                $(cancel).click();
            }
            if (e.which === 13 && !e.shiftKey) {
                $(submit).click();
                e.preventDefault();
            }
        });

        setTimeout(function () {
            $(textarea).focus();
        });

        return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), [
            h('div.cp-comment-form-input', [
                avatar,
                textarea
            ]),
            h('div.cp-comment-form-actions', [
                cancel,
                submit
            ])
        ]);
    };

    var redrawComments = function (Env) {
        // Don't redraw if there were no change
        var str = Sortify(Env.comments || {});
        if (str === Env.oldComments) { return; }
        Env.oldComments = str;

        // XXX don't wipe inputs?

        var $oldInput = Env.$container.find('.cp-comment-form');
        if ($oldInput.length !== 1) { $oldInput = undefined; }

        Env.$container.html('');

        var show = false;

        if ($oldInput && !$oldInput.attr('data-uid')) {
            show = true;
            Env.$container.append($oldInput);
        }

        var order = Env.$inner.find('comment').map(function (i, el) {
            return el.getAttribute('data-uid');
        }).toArray();
        var done = [];

        order.forEach(function (key) {
            // Avoir duplicates
            if (done.indexOf(key) !== -1) { return; }
            done.push(key);

            var obj = Env.comments.data[key];
            if (!obj || obj.deleted || !Array.isArray(obj.m) || !obj.m.length) {
                return;
            }
            show = true;

            var content = [];
            obj.m.forEach(function (msg, i) {
                var author = (Env.comments.authors || {})[msg.u] || {};
                var name = Util.fixHTML(author.name || Messages.anonymous);
                var date = new Date(msg.t);
                var avatar = h('span.cp-avatar');
                Env.common.displayAvatar($(avatar), author.avatar, name);

                content.push(h('div.cp-comment'+(i === 0 ? '' : '.cp-comment-reply'), [
                    h('div.cp-comment-header', [
                        avatar,
                        h('span.cp-comment-metadata', [
                            h('span.cp-comment-author', name),
                            h('span.cp-comment-time', date.toLocaleString())
                        ])
                    ]),
                    h('div.cp-comment-content', [
                        msg.m
                    ])
                ]));

            });

            var reply = h('button.btn.btn-secondary', [
                h('i.fa.fa-reply'),
                Messages.comments_reply
            ]);
            var resolve = h('button.btn.btn-primary', [
                h('i.fa.fa-check'),
                Messages.comments_resolve
            ]);

            var actions;
            content.push(actions = h('div.cp-comment-actions', [
                reply,
                resolve
            ]));
            var $actions = $(actions);

            var div;
            Env.$container.append(div = h('div.cp-comment-container', {
                'data-uid': key,
                tabindex: 1
            }, content));
            var $div = $(div);

            $(reply).click(function (e) {
                e.stopPropagation();
                $actions.hide();
                var form = getCommentForm(Env, true, function (val) {
                    $(form).remove();
                    $actions.css('display', '');
                    if (!val) { return; }
                    var obj = Env.comments.data[key];
                    if (!obj || !Array.isArray(obj.m)) { return; }

                    // Get the value of the commented text
                    var res = Env.$inner.find('comment[data-uid="'+key+'"]').toArray();
                    var value = res.map(function (el) {
                        return el.innerText;
                    }).join('\n');

                    // Push the reply
                    var myId = updateAuthorData(Env);
                    obj.m.push({
                        u: myId,
                        t: +new Date(),
                        m: val,
                        v: value
                    });

                    // Send to chainpad
                    updateMetadata(Env);
                    Env.framework.localChange();
                });
                $div.append(form);
            });

            UI.confirmButton(resolve, {
                classes: 'btn-danger-alt'
            }, function () {
                // Delete the comment
                delete Env.comments.data[key];

                // Send to chainpad
                updateMetadata(Env);
                Env.framework.localChange();
            });

            $div.click(function () {
                if ($div.hasClass('cp-comment-active')) { return; }
                Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
                $div.addClass('cp-comment-active');
                $actions.css('display', '');
                Env.$container.find('.cp-comment-form').remove();
                // XXX highlight (and scroll to) the comment in the doc?
            });
        });

        if (show) {
            Env.$container.show();
        } else {
            Env.$container.hide();
        }
    };

    var onChange = function (Env) {
        var md = Util.clone(Env.metadataMgr.getMetadata());
        Env.comments = md.comments;
        var changed = false;
        if (!Env.comments || !Env.comments.data) {
            changed = true;
            Env.comments = Util.clone(COMMENTS);
        }
        if (Env.ready === 0) {
            Env.ready = true;
            if (changed) {
                updateMetadata(Env);
                Env.framework.localChange();
            }
        }
        redrawComments(Env);
    };

    // Check if comments have been deleted from the document but not from metadata
    var checkDeleted = function (Env) {
        if (!Env.comments || !Env.comments.data) { return; }

        // Don't recheck if there were no change
        var str = Env.$inner[0].innerHTML;
        if (str === Env.oldCheck) { return; }
        Env.oldCheck = str;

        // If there is no comment stored in the metadata, abort
        var comments = Object.keys(Env.comments.data || {}).filter(function (id) {
            return !Env.comments.data[id].deleted;
        });

        var changed = false;

        // Get the comments from the document
        var uids = Env.$inner.find('comment').map(function (i, el) {
            var id = el.getAttribute('data-uid');
            // Empty comment: remove from dom
            if (!el.innerText && el.parentElement) {
                el.parentElement.removeChild(el);
                changed = true;
                return;
            }
            // Comment not in the metadata: uncomment (probably an undo)
            if (comments.indexOf(id) === -1) {
                Env.editor.plugins.comments.uncomment(id);
                changed = true;
                return;
            }
            return id;
        }).toArray();

        // Check if a comment has been deleted
        comments.forEach(function (uid) {
            if (uids.indexOf(uid) !== -1) { return; }
            // comment has been deleted
            var data = Env.comments.data[uid];
            if (!data) { return; }
            //data.deleted = true;
            delete Env.comments.data[uid];
            changed = true;
        });

        if (changed) {
            updateMetadata(Env);
        }
    };

    var addAddCommentHandler = function (Env) {
        Env.editor.plugins.comments.addComment = function (uid, addMark) {
            if (!Env.comments) { Env.comments = Util.clone(COMMENTS); }

            // Get all comments ID contained within the selection
            var sel = Env.editor.getSelectedHtml().$.querySelectorAll('comment');
            if (sel.length) {
                // Abort if our selection contains a comment
                console.error("Your selection contains a comment");
                UI.warn(Messages.error);
                // XXX show error
                return;
            }

/*
sel.forEach(function (el) {
    // For each comment ID, check if the comment will be deleted
    // if we add a comment on our selection
    var id = el.getAttribute('data-uid');

    // Get all nodes for this comment
    var all = Env.$inner.find('comment[data-uid="'+id+'"]');
    // Get our selection
    var sel = Env.ifrWindow.getSelection();
    if (!sel.containsNode) {
        // IE doesn't support this method, always allow comments for them...
        sel.containsNode = function () { return false; };
    }

    var notDeleted = all.some(function (i, el) {
        // If this node is completely outside of the selection, continue
        if (!sel.containsNode(el, true)) { return true; }
    });

    // only continue if notDeleted is true (at least one node for
    // this comment won't be deleted)
});
*/
            Env.$container.find('.cp-comment-form').remove();
            var form = getCommentForm(Env, false, function (val) {
                $(form).remove();
                Env.$inner.focus();

                if (!val) { return; }
                if (!editor.getSelection().getSelectedText()) {
                    // text has been deleted by another user while we were typing our comment?
                    return void UI.warn(Messages.error);
                }
                var myId = updateAuthorData(Env);
                Env.comments.data[uid] = {
                    m: [{
                        u: myId,
                        t: +new Date(),
                        m: val,
                        v: canonicalize(editor.getSelection().getSelectedText())
                    }]
                };
                updateMetadata(Env);

                addMark();

                Env.framework.localChange();
            });
            Env.$container.prepend(form).show();;
        };
    };

    var onContentUpdate = function (Env) {
        if (!Env.ready) { return; }
        // Check deleted
        onChange(Env);
        checkDeleted(Env);
    };
    var localChange = function (Env) {
        if (!Env.ready) { return; }
        // Check deleted
        checkDeleted(Env);
    };

    var ready = function (Env) {
        Env.ready = 0;
    };

    Comments.create = function (cfg) {
        var Env = cfg;
        Env.comments = Util.clone(COMMENTS);

        addAddCommentHandler(Env);

        $(window).click(function (e) {
            if ($(e.target).closest('.cp-comment-container').length) {
                return;
            }
            Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
        });

        var call = function (f) {
            return function () {
                try {
                    [].unshift.call(arguments, Env);
                    return f.apply(null, arguments);
                } catch (e) {
                    console.error(e);
                }
            };
        };

        Env.metadataMgr.onChange(call(onChange));

        return {
            onContentUpdate: call(onContentUpdate),
            localChange: call(localChange),
            ready: call(ready)
        };
    };

    return Comments;
});
<commit_msg>Recover previous comment input on redraw<commit_after>define([
    'json.sortify',
    '/common/common-util.js',
    '/common/hyperscript.js',
    '/common/common-interface.js',
    '/customize/messages.js'
], function (Sortify, Util, h, UI, Messages) {
    var Comments = {};

/*
{
    authors: {
        "id": {
            name: "",
            curvePublic: "",
            avatar: "",
            profile: ""
        }
    },
    data: {
        "uid": {
            m: [{
                u: id,
                m: "str", // comment
                t: +new Date,
                v: "str" // value of the commented content
            }],
            (deleted: undefined/true,)
        }
    }
}
*/

    var COMMENTS = {
        authors: {},
        data: {}
    };

    var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };

    // XXX function duplicated from www/code/markers.js
    var authorUid = function (existing) {
        if (!Array.isArray(existing)) { existing = []; }
        var n;
        var i = 0;
        while (!n || existing.indexOf(n) !== -1 && i++ < 1000) {
            n = Math.floor(Math.random() * 1000000);
        }
        // If we can't find a valid number in 1000 iterations, use 0...
        if (existing.indexOf(n) !== -1) { n = 0; }
        return n;
    };
    var getAuthorId = function (Env, curve) {
        var existing = Object.keys(Env.comments.authors || {}).map(Number);
        if (!Env.common.isLoggedIn()) { return authorUid(existing); }

        var uid;
        existing.some(function (id) {
            var author = Env.comments.authors[id] || {};
            if (author.curvePublic !== curve) { return; }
            uid = Number(id);
            return true;
        });
        return uid || authorUid(existing);
    };

    var updateAuthorData = function (Env) {
        var userData = Env.metadataMgr.getUserData();
        var myAuthorId = getAuthorId(Env, userData.curvePublic);
        var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {};
        data.name = userData.name;
        data.avatar = userData.avatar;
        data.profile = userData.profile;
        data.curvePublic = userData.curvePublic;
        console.log(data);
        return myAuthorId;
    };

    var updateMetadata = function (Env) {
        var md = Util.clone(Env.metadataMgr.getMetadata());
        md.comments = Util.clone(Env.comments);
        Env.metadataMgr.updateMetadata(md);
    };

    Messages.comments_submit = "Submit"; // XXX
    Messages.comments_reply = "Reply"; // XXX
    Messages.comments_resolve = "Resolve"; // XXX

    var getCommentForm = function (Env, reply, _cb) {
        var cb = Util.once(_cb);
        var userData = Env.metadataMgr.getUserData();
        var name = Util.fixHTML(userData.name || Messages.anonymous);
        var avatar = h('span.cp-avatar');
        var textarea = h('textarea');
        Env.common.displayAvatar($(avatar), userData.avatar, name);

        var cancel = h('button.btn.btn-cancel', [
            h('i.fa.fa-times'),
            Messages.cancel
        ]);
        var submit = h('button.btn.btn-primary', [
            h('i.fa.fa-paper-plane-o'),
            Messages.comments_submit
        ]);

        var done = false;

        $(submit).click(function (e) {
            e.stopPropagation();
            cb(textarea.value);
        });
        $(cancel).click(function (e) {
            e.stopPropagation();
            cb();
        });

        $(textarea).keydown(function (e) {
            if (e.which === 27) {
                $(cancel).click();
            }
            if (e.which === 13 && !e.shiftKey) {
                $(submit).click();
                e.preventDefault();
            }
        });

        setTimeout(function () {
            $(textarea).focus();
        });

        return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), {
            'data-uid': reply || undefined
        }, [
            h('div.cp-comment-form-input', [
                avatar,
                textarea
            ]),
            h('div.cp-comment-form-actions', [
                cancel,
                submit
            ])
        ]);
    };

    var redrawComments = function (Env) {
        // Don't redraw if there were no change
        var str = Sortify(Env.comments || {});
        if (str === Env.oldComments) { return; }
        Env.oldComments = str;

        var $oldInput = Env.$container.find('.cp-comment-form').detach();
        if ($oldInput.length !== 1) { $oldInput = undefined; }

        Env.$container.html('');

        var show = false;

        if ($oldInput && !$oldInput.attr('data-uid')) {
            show = true;
            Env.$container.append($oldInput);
        }

        var order = Env.$inner.find('comment').map(function (i, el) {
            return el.getAttribute('data-uid');
        }).toArray();
        var done = [];

        order.forEach(function (key) {
            // Avoir duplicates
            if (done.indexOf(key) !== -1) { return; }
            done.push(key);

            var obj = Env.comments.data[key];
            if (!obj || obj.deleted || !Array.isArray(obj.m) || !obj.m.length) {
                return;
            }
            show = true;

            var content = [];
            obj.m.forEach(function (msg, i) {
                var author = (Env.comments.authors || {})[msg.u] || {};
                var name = Util.fixHTML(author.name || Messages.anonymous);
                var date = new Date(msg.t);
                var avatar = h('span.cp-avatar');
                Env.common.displayAvatar($(avatar), author.avatar, name);

                content.push(h('div.cp-comment'+(i === 0 ? '' : '.cp-comment-reply'), [
                    h('div.cp-comment-header', [
                        avatar,
                        h('span.cp-comment-metadata', [
                            h('span.cp-comment-author', name),
                            h('span.cp-comment-time', date.toLocaleString())
                        ])
                    ]),
                    h('div.cp-comment-content', [
                        msg.m
                    ])
                ]));

            });

            var reply = h('button.btn.btn-secondary', [
                h('i.fa.fa-reply'),
                Messages.comments_reply
            ]);
            var resolve = h('button.btn.btn-primary', [
                h('i.fa.fa-check'),
                Messages.comments_resolve
            ]);

            var actions;
            content.push(actions = h('div.cp-comment-actions', [
                reply,
                resolve
            ]));
            var $actions = $(actions);

            var div;
            Env.$container.append(div = h('div.cp-comment-container', {
                'data-uid': key,
                tabindex: 1
            }, content));
            var $div = $(div);

            $(reply).click(function (e) {
                e.stopPropagation();
                $actions.hide();
                var form = getCommentForm(Env, key, function (val) {
                    $(form).remove();
                    $(form).closest('.cp-comment-container')
                        .find('.cp-comment-actions').css('display', '');

                    if (!val) { return; }
                    var obj = Env.comments.data[key];
                    if (!obj || !Array.isArray(obj.m)) { return; }

                    // Get the value of the commented text
                    var res = Env.$inner.find('comment[data-uid="'+key+'"]').toArray();
                    var value = res.map(function (el) {
                        return el.innerText;
                    }).join('\n');

                    // Push the reply
                    var myId = updateAuthorData(Env);
                    obj.m.push({
                        u: myId,
                        t: +new Date(),
                        m: val,
                        v: value
                    });

                    // Send to chainpad
                    updateMetadata(Env);
                    Env.framework.localChange();
                });
                $div.append(form);
            });

            UI.confirmButton(resolve, {
                classes: 'btn-danger-alt'
            }, function () {
                // Delete the comment
                delete Env.comments.data[key];
                Env.editor.plugins.comments.uncomment(key);

                // Send to chainpad
                updateMetadata(Env);
                Env.framework.localChange();
            });

            $div.click(function () {
                if ($div.hasClass('cp-comment-active')) { return; }
                Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
                $div.addClass('cp-comment-active');
                $actions.css('display', '');
                Env.$container.find('.cp-comment-form').remove();
                // XXX highlight (and scroll to) the comment in the doc?
            });

            if ($oldInput && $oldInput.attr('data-uid') === key) {
                $div.addClass('cp-comment-active');
                $actions.hide();
                $div.append($oldInput);
                $oldInput.find('textarea').focus();
            }
        });

        if (show) {
            Env.$container.show();
        } else {
            Env.$container.hide();
        }
    };

    var onChange = function (Env) {
        var md = Util.clone(Env.metadataMgr.getMetadata());
        Env.comments = md.comments;
        var changed = false;
        if (!Env.comments || !Env.comments.data) {
            changed = true;
            Env.comments = Util.clone(COMMENTS);
        }
        if (Env.ready === 0) {
            Env.ready = true;
            if (changed) {
                updateMetadata(Env);
                Env.framework.localChange();
            }
        }
        redrawComments(Env);
    };

    // Check if comments have been deleted from the document but not from metadata
    var checkDeleted = function (Env) {
        if (!Env.comments || !Env.comments.data) { return; }

        // Don't recheck if there were no change
        var str = Env.$inner[0].innerHTML;
        if (str === Env.oldCheck) { return; }
        Env.oldCheck = str;

        // If there is no comment stored in the metadata, abort
        var comments = Object.keys(Env.comments.data || {}).filter(function (id) {
            return !Env.comments.data[id].deleted;
        });

        var changed = false;

        // Get the comments from the document
        var uids = Env.$inner.find('comment').map(function (i, el) {
            var id = el.getAttribute('data-uid');
            // Empty comment: remove from dom
            if (!el.innerText && el.parentElement) {
                el.parentElement.removeChild(el);
                changed = true;
                return;
            }
            // Comment not in the metadata: uncomment (probably an undo)
            if (comments.indexOf(id) === -1) {
                Env.editor.plugins.comments.uncomment(id);
                changed = true;
                return;
            }
            return id;
        }).toArray();

        // Check if a comment has been deleted
        comments.forEach(function (uid) {
            if (uids.indexOf(uid) !== -1) { return; }
            // comment has been deleted
            var data = Env.comments.data[uid];
            if (!data) { return; }
            //data.deleted = true;
            delete Env.comments.data[uid];
            changed = true;
        });

        if (changed) {
            updateMetadata(Env);
        }
    };

    var addAddCommentHandler = function (Env) {
        Env.editor.plugins.comments.addComment = function (uid, addMark) {
            if (!Env.comments) { Env.comments = Util.clone(COMMENTS); }

            // Get all comments ID contained within the selection
            var sel = Env.editor.getSelectedHtml().$.querySelectorAll('comment');
            if (sel.length) {
                // Abort if our selection contains a comment
                console.error("Your selection contains a comment");
                UI.warn(Messages.error);
                // XXX show error
                return;
            }

/*
sel.forEach(function (el) {
    // For each comment ID, check if the comment will be deleted
    // if we add a comment on our selection
    var id = el.getAttribute('data-uid');

    // Get all nodes for this comment
    var all = Env.$inner.find('comment[data-uid="'+id+'"]');
    // Get our selection
    var sel = Env.ifrWindow.getSelection();
    if (!sel.containsNode) {
        // IE doesn't support this method, always allow comments for them...
        sel.containsNode = function () { return false; };
    }

    var notDeleted = all.some(function (i, el) {
        // If this node is completely outside of the selection, continue
        if (!sel.containsNode(el, true)) { return true; }
    });

    // only continue if notDeleted is true (at least one node for
    // this comment won't be deleted)
});
*/
            Env.$container.find('.cp-comment-form').remove();
            var form = getCommentForm(Env, false, function (val) {
                $(form).remove();
                Env.$inner.focus();

                if (!val) { return; }
                if (!editor.getSelection().getSelectedText()) {
                    // text has been deleted by another user while we were typing our comment?
                    return void UI.warn(Messages.error);
                }
                // Don't override existing data
                if (Env.comments.data[uid]) { return; }

                var myId = updateAuthorData(Env);
                Env.comments.data[uid] = {
                    m: [{
                        u: myId,
                        t: +new Date(),
                        m: val,
                        v: canonicalize(editor.getSelection().getSelectedText())
                    }]
                };
                updateMetadata(Env);

                addMark();

                Env.framework.localChange();
            });
            Env.$container.prepend(form).show();;
        };
    };

    var onContentUpdate = function (Env) {
        if (!Env.ready) { return; }
        // Check deleted
        onChange(Env);
        checkDeleted(Env);
    };
    var localChange = function (Env) {
        if (!Env.ready) { return; }
        // Check deleted
        checkDeleted(Env);
    };

    var ready = function (Env) {
        Env.ready = 0;
    };

    Comments.create = function (cfg) {
        var Env = cfg;
        Env.comments = Util.clone(COMMENTS);

        addAddCommentHandler(Env);

        $(window).click(function (e) {
            if ($(e.target).closest('.cp-comment-container').length) {
                return;
            }
            Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
        });

        var call = function (f) {
            return function () {
                try {
                    [].unshift.call(arguments, Env);
                    return f.apply(null, arguments);
                } catch (e) {
                    console.error(e);
                }
            };
        };

        Env.metadataMgr.onChange(call(onChange));

        return {
            onContentUpdate: call(onContentUpdate),
            localChange: call(localChange),
            ready: call(ready)
        };
    };

    return Comments;
});
<|endoftext|>


<commit_before>var express       = require('express'),
    bodyParser    = require('body-parser'),
    cors          = require('cors'),
    fayeWebsocket = require('faye-websocket'),
    EventEmitter  = require('events').EventEmitter,
    EventSource   = fayeWebsocket.EventSource;

var Match   = require('./match');

module.exports = function (events, db) {

  var Users   = require('./users-db')(db),
      Players = require('./players-db')(db),
      MatchDb = require('./match-db')(db),
      Rating = require('./rating')(db);

  var match;
  var browserEvents = new EventEmitter();

  var app = express();

  app.use(cors());
  app.use(bodyParser.json({
    reviver: require('./json-parse-date')
  }));
  app.use(app.router);
  app.use(logErrors);
  app.use(clientErrorHandler);

  app.get('/status', function (req, res, next) {
    respond(res);
  });

  app.post('/start', function (req, res, next) {
    var body = req.body;

    match = new Match(events, body.a, body.b, Match.LengthSeconds);
    match.on('change', function () {
      sendSseEventScore();
      if (match && match.done) {
        MatchDb.save(match);

        Players.del(body.a.number);
        Players.del(body.b.number);

        Rating.calculateRating(function (err, ranking){
          if (err) return next(err);
          sendSseEventRankingUpdated(ranking);
        });

        sendSseEventMatch(match);
        sendSseEventPlayers(sendSseEventWinner);
      }
    });
    match.start();
    respond(res);

    sendSseEventScore();
  });

  app.post('/stop', function (req, res, next) {
    if (match) {
      match.stop();
      match = null;
    }
    respond(res);

    sendSseEventScore();
  });

  app.post('/signup', function (req, res, next) {
    var user = req.body;

    Users.save(user, function (err) {
      if (err) return next(err);

      Players.add(user, function (err) {
        if (err) return next(err);

        res.json(user);
        sendSseEventPlayers();
      });
    });
  });

  app.get('/users', function (req, res, next) {
    Users.all(function (err, users) {
      if (err) return next(err);
      res.json(users);
    });
  });

  app.get('/users/:number', function (req, res, next) {
    var number = req.param("number");
    Users.get(number, function (err, user) {
      if (err && err.notFound) return res.json({});
      if (err) return next(err);
      res.json(user);
    });
  });

  app.get('/players', function (req, res, next) {
    Players.all(function (err, players) {
      if (err) return next(err);
      res.json(players);
    });
  });

  app.del('/players/:number', function (req, res, next) {
    var number = req.param('number');
    Players.del(number, function (err) {
      if (err) return next(err);
      sendSseEventPlayers();
      res.end('ok');
    });
  });

  app.get('/matches', function (req, res, next) {
    MatchDb.all(function (err, matches) {
      if (err) return next(err);
      res.json(matches);
    });
  });

  app.get('/ranking', function (req, res, next) {
    Rating.getRating(function(err, ratings) {
      if (err) return next(err);
      res.json(ratings);
    });
  });

  app.post('/matches/:time/score', function (req, res, next) {
    var score = req.body;
    var time = req.param('time');
    MatchDb.updateScore(time, score, function () {
      res.json(score);
    });
  });

  app.get('/connect', function (req, res, next) {
    res.end('ok');

    sendSseEventScore();
    sendSseEventPlayers();
    if (connected) {
      sendSseEventConnected();
    }
    else {
      sendSseEventDisconnected();
    }
  });

  function respond (res) {
    if (match) {
      res.json(match.json());
      return;
    }
    res.json({});
  }

  function sendSseEvent (type, data) {
    browserEvents.emit('browser-event', { type: type, data: data });
  }
  function sendSseEventConnected() {
    sendSseEvent('connected', {});
  }
  function sendSseEventDisconnected() {
    sendSseEvent('disconnected', {});
  }
  function sendSseEventScore () {
    sendSseEvent('score', match ? match.json() : {});
  }
  function sendSseEventPlayers (fn) {
    Players.all(function (err, players) {
      if (err) return console.error(err);
      sendSseEvent('players', players);
      if (fn) fn();
    });
  }
  function sendSseEventWinner () {
    sendSseEvent('winner');
  }
  function sendSseEventMatch (match) {
    sendSseEvent('match', match.json());
  }
  function sendSseEventRankingUpdated (ranking) {
    sendSseEvent('ranking', ranking);
  }

  var connected = false;
  events.on('connected', function () {
    sendSseEventScore();
    sendSseEventConnected();
    connected = true;
  });
  events.on('disconnected', function () {
    sendSseEventScore();
    sendSseEventDisconnected();
    connected = false;
  });

  // sockets
  app.get('/es', function(req, res) {
    if (EventSource.isEventSource(req)) {

      var es = new EventSource(req, res, {
        headers: { 'Access-Control-Allow-Origin': '*' },
        ping:    15, // seconds
        retry:   3   // seconds
      });

      var id = parseInt(es.lastEventId, 10) || 0;
      var sendEvent = function sendEvent (e) {
        es.send(JSON.stringify(e.data), { event: e.type, id: ++id });
      };

      browserEvents.on('browser-event', sendEvent);

      es.on('close', function() {
        es = null;
        browserEvents.off('browser-event', sendEvent);
      });
    }
  });

  function logErrors (err, req, res, next) {
    console.error(err.stack);
    next(err);
  }

  function clientErrorHandler (err, req, res, next) {
    res.status(500).send({ error: err });
  }

  return app;
};
<commit_msg>Send ranking on /connect.<commit_after>var express       = require('express'),
    bodyParser    = require('body-parser'),
    cors          = require('cors'),
    fayeWebsocket = require('faye-websocket'),
    EventEmitter  = require('events').EventEmitter,
    EventSource   = fayeWebsocket.EventSource;

var Match   = require('./match');

module.exports = function (events, db) {

  var Users   = require('./users-db')(db),
      Players = require('./players-db')(db),
      MatchDb = require('./match-db')(db),
      Rating = require('./rating')(db);

  var match;
  var browserEvents = new EventEmitter();

  var app = express();

  app.use(cors());
  app.use(bodyParser.json({
    reviver: require('./json-parse-date')
  }));
  app.use(app.router);
  app.use(logErrors);
  app.use(clientErrorHandler);

  app.get('/status', function (req, res, next) {
    respond(res);
  });

  app.post('/start', function (req, res, next) {
    var body = req.body;

    match = new Match(events, body.a, body.b, Match.LengthSeconds);
    match.on('change', function () {
      sendSseEventScore();
      if (match && match.done) {
        MatchDb.save(match);

        Players.del(body.a.number);
        Players.del(body.b.number);

        Rating.calculateRating(function (err, ranking){
          if (err) return next(err);
          sendSseEventRankingUpdated(ranking);
        });

        sendSseEventMatch(match);
        sendSseEventPlayers(sendSseEventWinner);
      }
    });
    match.start();
    respond(res);

    sendSseEventScore();
  });

  app.post('/stop', function (req, res, next) {
    if (match) {
      match.stop();
      match = null;
    }
    respond(res);

    sendSseEventScore();
  });

  app.post('/signup', function (req, res, next) {
    var user = req.body;

    Users.save(user, function (err) {
      if (err) return next(err);

      Players.add(user, function (err) {
        if (err) return next(err);

        res.json(user);
        sendSseEventPlayers();
      });
    });
  });

  app.get('/users', function (req, res, next) {
    Users.all(function (err, users) {
      if (err) return next(err);
      res.json(users);
    });
  });

  app.get('/users/:number', function (req, res, next) {
    var number = req.param("number");
    Users.get(number, function (err, user) {
      if (err && err.notFound) return res.json({});
      if (err) return next(err);
      res.json(user);
    });
  });

  app.get('/players', function (req, res, next) {
    Players.all(function (err, players) {
      if (err) return next(err);
      res.json(players);
    });
  });

  app.del('/players/:number', function (req, res, next) {
    var number = req.param('number');
    Players.del(number, function (err) {
      if (err) return next(err);
      sendSseEventPlayers();
      res.end('ok');
    });
  });

  app.get('/matches', function (req, res, next) {
    MatchDb.all(function (err, matches) {
      if (err) return next(err);
      res.json(matches);
    });
  });

  app.get('/ranking', function (req, res, next) {
    Rating.getRating(function(err, ratings) {
      if (err) return next(err);
      res.json(ratings);
    });
  });

  app.post('/matches/:time/score', function (req, res, next) {
    var score = req.body;
    var time = req.param('time');
    MatchDb.updateScore(time, score, function () {
      res.json(score);
    });
  });

  app.get('/connect', function (req, res, next) {
    res.end('ok');

    sendSseEventScore();
    sendSseEventPlayers();

    if (connected) {
      sendSseEventConnected();
    }
    else {
      sendSseEventDisconnected();
    }

    Rating.calculateRating(function (err, ranking){
      if (err) return next(err);
      sendSseEventRankingUpdated(ranking);
    });
  });

  function respond (res) {
    if (match) {
      res.json(match.json());
      return;
    }
    res.json({});
  }

  function sendSseEvent (type, data) {
    browserEvents.emit('browser-event', { type: type, data: data });
  }
  function sendSseEventConnected() {
    sendSseEvent('connected', {});
  }
  function sendSseEventDisconnected() {
    sendSseEvent('disconnected', {});
  }
  function sendSseEventScore () {
    sendSseEvent('score', match ? match.json() : {});
  }
  function sendSseEventPlayers (fn) {
    Players.all(function (err, players) {
      if (err) return console.error(err);
      sendSseEvent('players', players);
      if (fn) fn();
    });
  }
  function sendSseEventWinner () {
    sendSseEvent('winner');
  }
  function sendSseEventMatch (match) {
    sendSseEvent('match', match.json());
  }
  function sendSseEventRankingUpdated (ranking) {
    sendSseEvent('ranking', ranking);
  }

  var connected = false;
  events.on('connected', function () {
    sendSseEventScore();
    sendSseEventConnected();
    connected = true;
  });
  events.on('disconnected', function () {
    sendSseEventScore();
    sendSseEventDisconnected();
    connected = false;
  });

  // sockets
  app.get('/es', function(req, res) {
    if (EventSource.isEventSource(req)) {

      var es = new EventSource(req, res, {
        headers: { 'Access-Control-Allow-Origin': '*' },
        ping:    15, // seconds
        retry:   3   // seconds
      });

      var id = parseInt(es.lastEventId, 10) || 0;
      var sendEvent = function sendEvent (e) {
        es.send(JSON.stringify(e.data), { event: e.type, id: ++id });
      };

      browserEvents.on('browser-event', sendEvent);

      es.on('close', function() {
        es = null;
        browserEvents.off('browser-event', sendEvent);
      });
    }
  });

  function logErrors (err, req, res, next) {
    console.error(err.stack);
    next(err);
  }

  function clientErrorHandler (err, req, res, next) {
    res.status(500).send({ error: err });
  }

  return app;
};
<|endoftext|>


<commit_before>const yauzl = require('yauzl');
const yazl = require('yazl');
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const glob = require('glob');

function zip(sourceDir, targetFileName, cb) {
  sourceDir = sourceDir.replace(/\/?$/, '/');
  glob(`${sourceDir}**/*`, { nodir: true, dot: true }, function (err, files) {
    if (err) return cb && cb(err);
    const zipfile = new yazl.ZipFile();
    (files || []).forEach(function (file) {
      const relativeFileName = path.relative(sourceDir, file);
      zipfile.addFile(file, relativeFileName);
    });
    const writeStream = fs.createWriteStream(targetFileName);
    writeStream.on('error', err => cb && cb(err));
    writeStream.on('finish', () => cb && cb());
    zipfile.outputStream.pipe(writeStream);
    zipfile.end();
  });
}

function unzip(sourceFileName, targetDir, cb) {
  yauzl.open(sourceFileName, { lazyEntries: true }, function (err, zipfile) {
    if (err) return cb && cb(err);
    zipfile
      .on('entry', function (entry) {
        const unzippedFileName = path.join(targetDir, entry.fileName);
        if (/\/$/.test(unzippedFileName)) {
          // directory file names end with '/'
          mkdirp(unzippedFileName, function (err) {
            if (err) return cb && cb(err);
            zipfile.readEntry();
          });
        } else {
          // file entry
          zipfile.openReadStream(entry, function (err, readStream) {
            if (err) return cb && cb(err);
            // ensure parent directory exists
            mkdirp(path.dirname(unzippedFileName), function (err) {
              if (err) return cb && cb(err);
              readStream.pipe(fs.createWriteStream(unzippedFileName));
              readStream.on('end', () => zipfile.readEntry());
              readStream.on('error', err => cb && cb(err));
            });
          });
        }
      })
      .on('error', function (err) {
        zipfile.close();
        return cb && cb(err);
      })
      .on('end', function () {
        zipfile.close();
        return cb && cb();
      });
    zipfile.readEntry();
  });
}

module.exports = { zip, unzip };
<commit_msg>Fix error from breaking change in mkdirp<commit_after>const yauzl = require('yauzl');
const yazl = require('yazl');
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const glob = require('glob');

function zip(sourceDir, targetFileName, cb) {
  sourceDir = sourceDir.replace(/\/?$/, '/');
  glob(`${sourceDir}**/*`, { nodir: true, dot: true }, function (err, files) {
    if (err) return cb && cb(err);
    const zipfile = new yazl.ZipFile();
    (files || []).forEach(function (file) {
      const relativeFileName = path.relative(sourceDir, file);
      zipfile.addFile(file, relativeFileName);
    });
    const writeStream = fs.createWriteStream(targetFileName);
    writeStream.on('error', err => cb && cb(err));
    writeStream.on('finish', () => cb && cb());
    zipfile.outputStream.pipe(writeStream);
    zipfile.end();
  });
}

function unzip(sourceFileName, targetDir, cb) {
  yauzl.open(sourceFileName, { lazyEntries: true }, function (err, zipfile) {
    if (err) return cb && cb(err);
    zipfile
      .on('entry', function (entry) {
        const unzippedFileName = path.join(targetDir, entry.fileName);
        if (/\/$/.test(unzippedFileName)) {
          // directory file names end with '/'
          mkdirp(unzippedFileName)
            .then(() => zipfile.readEntry())
            .catch(err => cb && cb(err));
        } else {
          // file entry
          zipfile.openReadStream(entry, function (err1, readStream) {
            if (err1) return cb && cb(err1);
            // ensure parent directory exists
            mkdirp(path.dirname(unzippedFileName))
              .then(() => {
                readStream.pipe(fs.createWriteStream(unzippedFileName));
                readStream.on('end', () => zipfile.readEntry());
                readStream.on('error', err2 => cb && cb(err2));
              })
              .catch(err3 => cb && cb(err3));
          });
        }
      })
      .on('error', function (err) {
        zipfile.close();
        return cb && cb(err);
      })
      .on('end', function () {
        zipfile.close();
        return cb && cb();
      });
    zipfile.readEntry();
  });
}

module.exports = { zip, unzip };
<|endoftext|>


<commit_before>const path = require('path')
const favicon = require('serve-favicon')
const compress = require('compression')
const cors = require('cors')
const helmet = require('helmet')
const bodyParser = require('body-parser')

const feathers = require('feathers')
const configuration = require('feathers-configuration')
const hooks = require('feathers-hooks')
const rest = require('feathers-rest')
const socketio = require('feathers-socketio')

const middleware = require('./middleware')
const services = require('./services')
const appHooks = require('./app.hooks')

const authentication = require('./authentication')

const app = feathers()

// Load app configuration
app.configure(configuration(path.join(__dirname, '..')))

// Enable CORS, security, compression, favicon and body parsing
app.use(cors())
app.use(helmet())
app.use(compress())
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(favicon(path.join(app.get('public'), 'assets', 'favicon.ico')))

app.get('/', serveHtmlForEnvironment)

// Host the public folder
app.use('/assets', feathers.static(path.join(app.get('public'), 'assets')))
if (process.env.NODE_ENV === 'production') {
  app.use('/dist', feathers.static(path.join(app.get('public'), 'dist')))
}

// Set up Plugins and providers
app.configure(hooks())
app.configure(rest())
app.configure(socketio())

app.configure(authentication)

// Set up our services (see `services/index.js`)
app.configure(services)

// Configure middleware (see `middleware/index.js`) - always has to be last
app.configure(middleware)
app.hooks(appHooks)

// catch all for react
app.get('*', serveHtmlForEnvironment)

module.exports = app

function serveHtmlForEnvironment (req, res) {
  var html = path.join(app.get('public'), process.env.NODE_ENV === 'development' ? 'index-dev.html' : 'index.html')
  res.sendFile(html)
}
<commit_msg>try with proces.cwd<commit_after>const path = require('path')
const favicon = require('serve-favicon')
const compress = require('compression')
const cors = require('cors')
const helmet = require('helmet')
const bodyParser = require('body-parser')

const feathers = require('feathers')
const configuration = require('feathers-configuration')
const hooks = require('feathers-hooks')
const rest = require('feathers-rest')
const socketio = require('feathers-socketio')

const middleware = require('./middleware')
const services = require('./services')
const appHooks = require('./app.hooks')

const authentication = require('./authentication')

const app = feathers()

// Load app configuration
app.configure(configuration(path.join(process.cwd(), '..')))

// Enable CORS, security, compression, favicon and body parsing
app.use(cors())
app.use(helmet())
app.use(compress())
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(favicon(path.join(app.get('public'), 'assets', 'favicon.ico')))

app.get('/', serveHtmlForEnvironment)

// Host the public folder
app.use('/assets', feathers.static(path.join(app.get('public'), 'assets')))
if (process.env.NODE_ENV === 'production') {
  app.use('/dist', feathers.static(path.join(app.get('public'), 'dist')))
}

// Set up Plugins and providers
app.configure(hooks())
app.configure(rest())
app.configure(socketio())

app.configure(authentication)

// Set up our services (see `services/index.js`)
app.configure(services)

// Configure middleware (see `middleware/index.js`) - always has to be last
app.configure(middleware)
app.hooks(appHooks)

// catch all for react
app.get('*', serveHtmlForEnvironment)

module.exports = app

function serveHtmlForEnvironment (req, res) {
  var html = path.join(app.get('public'), process.env.NODE_ENV === 'development' ? 'index-dev.html' : 'index.html')
  res.sendFile(html)
}
<|endoftext|>


<commit_before>//https://serverless.com/framework/docs/providers/aws/guide/testing/

const uuid = require('uuid');

class xSharedFunctions {
    generateSuccessResponse(respCode, dataSuc, component){

            var responseCode = (respCode === null) ? 200:respCode ;
            var responseBody = "";

            var response = {
              statusCode: responseCode,
              body:       responseBody
            };

            response.body = {
                component: component,
                status:  "success",
                data: dataSuc
            };

            response.body = JSON.stringify(response.body);

            return response;
    }

    generateErrorResponse(respCode, dataErr, component){

            var responseCode = (respCode === null) ? 400:respCode ;
            var responseBody = "";

            var response = {
              statusCode: responseCode,
              body:       responseBody
            };

            response.body = {
                component: component,
                status:  "error",
                error:   dataErr,
            };

            response.body = JSON.stringify(response.body);

            return response;
    }

    isDef(v) {
        return v !== undefined && v !== null;
    }

    logmsg(component, uniqueId, severity,  message) {
        var timestamp = new Date().getTime();
        console.log(`[${component}] [${timestamp}] [${uniqueId}][${severity}] ${message}`);
    }

}

module.exports = xSharedFunctions;

<commit_msg>null params as lat one in functions<commit_after>//https://serverless.com/framework/docs/providers/aws/guide/testing/

const uuid = require('uuid');

class xSharedFunctions {
    generateSuccessResponse(dataSuc, component,respCode){

            var responseCode = (respCode === null) ? 200:respCode ;
            var responseBody = "";

            var response = {
              statusCode: responseCode,
              body:       responseBody
            };

            response.body = {
                component: component,
                status:  "success",
                data: dataSuc
            };

            response.body = JSON.stringify(response.body);

            return response;
    }

    generateErrorResponse(dataErr, component, respCode){

            var responseCode = (respCode === null) ? 400:respCode ;
            var responseBody = "";

            var response = {
              statusCode: responseCode,
              body:       responseBody
            };

            response.body = {
                component: component,
                status:  "error",
                error:   dataErr,
            };

            response.body = JSON.stringify(response.body);

            return response;
    }

    isDef(v) {
        return v !== undefined && v !== null;
    }

    logmsg(component, uniqueId, severity,  message) {
        var timestamp = new Date().getTime();
        console.log(`[${component}] [${timestamp}] [${uniqueId}][${severity}] ${message}`);
    }

}

module.exports = xSharedFunctions;

<|endoftext|>


<commit_before>// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

var errors = require('./errors.js');
var inherits = require('util').inherits;
var EventEmitter = require('./lib/event_emitter');

var TOMBSTONE_TTL_OFFSET = 500;
var MAX_TOMBSTONE_TTL = 5000;

module.exports = Operations;

function Operations(opts) {
    var self = this;

    EventEmitter.call(self);
    self.draining = false;
    self.drainEvent = self.defineEvent('drain');
    self.pendingChangeEvent = self.defineEvent('pendingChange');
    self.pendingChangeDelta = null;

    self.timers = opts.timers;
    self.logger = opts.logger;
    self.random = opts.random;
    self.connectionStalePeriod = opts.connectionStalePeriod;
    self.maxTombstoneTTL = MAX_TOMBSTONE_TTL;

    self.connection = opts.connection;
    // TODO need this?
    self.destroyed = false;

    self.requests = {
        in: Object.create(null),
        out: Object.create(null)
    };
    self.pending = {
        in: 0,
        out: 0,
        errors: 0
    };
    self.lastTimeoutTime = 0;
}
inherits(Operations, EventEmitter);

Operations.prototype.emitPendingChange =
function emitPendingChange(numIn, numOut, numErrors) {
    var self = this;

    if (self.pendingChangeDelta !== null) {
        self.pendingChangeDelta.in += numIn;
        self.pendingChangeDelta.out += numOut;
        self.pendingChangeDelta.errors += numErrors;
    } else {
        self.pendingChangeEvent.emit(self, {
            in: numIn,
            out: numOut,
            errors: numErrors
        });
    }
};

Operations.prototype.extendLogInfo =
function extendLogInfo(info) {
    var self = this;

    if (self.connection) {
        info = self.connection.extendLogInfo(info);
    }

    return info;
};

Operations.prototype.setMaxTombstoneTTL =
function setMaxTombstoneTTL(ttl) {
    var self = this;

    self.maxTombstoneTTL = ttl;
};

function OperationTombstone(operations, id, time, req, context) {
    var self = this;

    var timeout = Math.min(
        TOMBSTONE_TTL_OFFSET + req.timeout,
        operations.maxTombstoneTTL
    );

    self.type = 'tchannel.operation.tombstone';
    self.isTombstone = true;
    self.logger = operations.logger;
    self.operations = operations;
    self.id = id;
    self.time = time;
    self.timeout = timeout;
    self.timeHeapHandle = null;
    self.destroyed = false;
    self.serviceName = req.serviceName;
    self.callerName = req.callerName;
    self.endpoint = req.endpoint;

    self.isPendingError = false;
    if (context && context.isErrorFrame) {
        self.isPendingError = errors.isPendingError(context.codeName);
    }
}

OperationTombstone.prototype.extendLogInfo = function extendLogInfo(info) {
    var self = this;

    info.id = self.id;
    info.serviceName = self.serviceName;
    info.callerName = self.callerName;
    info.endpoint = self.endpoint;
    info.tombstoneTime = self.time;
    info.tombstoneTTL = self.timeout;
    info.heapCanceled = self.timeHeapHandle && !self.timeHeapHandle.item;
    info.heapExpireTime = self.timeHeapHandle && self.timeHeapHandle.expireTime;
    info.heapAmItem = self.timeHeapHandle && self.timeHeapHandle.item === self;

    if (self.operations) {
        info = self.operations.extendLogInfo(info);
        var other = self.operations.requests.out[self.id];
        if (self !== other) {
            info.otherType = typeof other;
            info.otherConstructorName = other && other.constructor && other.constructor.name;
        }
    }

    return info;
};

OperationTombstone.prototype.destroy = function destroy(now) {
    var self = this;

    self.destroyed = true;

    self.onTimeout(now);
};

OperationTombstone.prototype.onTimeout = function onTimeout(now) {
    var self = this;

    if (!self.destroyed && now < self.timeout + self.time) {
        self.logger.error('tombstone timed out too early', self.extendLogInfo({
            now: now,
            expireTime: self.timeout + self.time,
            delta: (self.timeout + self.time) - now
        }));
    }

    if (self.operations &&
        self.operations.requests.out[self.id] === self) {
        delete self.operations.requests.out[self.id];
        if (self.isPendingError) {
            self.operations.pending.errors--;
            self.operations.emitPendingChange(0, 0, -1);
        }
        self.operations = null;
    } else {
        self.logger.warn('mismatched expired operation tombstone', self.extendLogInfo({}));
        self.operations = null;
    }

    self.timeHeapHandle = null;
};

Operations.prototype.checkLastTimeoutTime = function checkLastTimeoutTime(now) {
    var self = this;

    if (self.lastTimeoutTime &&
        now > self.lastTimeoutTime + self.connectionStalePeriod
    ) {
        self._deferResetDueToTimeouts(now);
    } else if (!self.lastTimeoutTime) {
        self.lastTimeoutTime = now;
    }
};

Operations.prototype._deferResetDueToTimeouts = function _deferResetDueToTimeouts(now) {
    var self = this;

    var elapsed = now - self.lastTimeoutTime;
    var err = errors.ConnectionStaleTimeoutError({
        period: self.connectionStalePeriod,
        elapsed: elapsed,
        lastTimeoutTime: self.lastTimeoutTime
    });
    process.nextTick(opCheckLastTimedout);

    function opCheckLastTimedout() {
        self.logger.warn('destroying socket from timeouts', self.connection.extendLogInfo({
            error: err
        }));
        self.connection.resetAll(err);
    }
};

Operations.prototype.getRequests = function getRequests() {
    var self = this;

    return self.requests;
};

Operations.prototype.getPending = function getPending() {
    var self = this;

    return self.pending;
};

// TODO: Merge getOutTombstone() with getOutReq()
Operations.prototype.getOutTombstone = function getOutTombstone(id) {
    var self = this;

    var op = self.requests.out[id] || null;
    if (op && !op.isTombstone) {
        return null;
    }

    return op;
};

Operations.prototype.getOutReq = function getOutReq(id) {
    var self = this;

    var req = self.requests.out[id] || null;
    if (req && req.isTombstone) {
        return null;
    }

    return req;
};

Operations.prototype.getInReq = function getInReq(id) {
    var self = this;

    return self.requests.in[id];
};

Operations.prototype.addOutReq = function addOutReq(req) {
    var self = this;

    req.operations = self;
    self.requests.out[req.id] = req;
    self.pending.out++;

    req.timeHeapHandle = self.connection.channel.timeHeap.update(req);

    self.emitPendingChange(0, 1, 0);

    return req;
};

Operations.prototype.addInReq = function addInReq(req) {
    var self = this;

    req.operations = self;
    self.requests.in[req.id] = req;
    self.pending.in++;

    req.timeHeapHandle = self.connection.channel.timeHeap.update(req);

    self.emitPendingChange(1, 0, 0);

    return req;
};

Operations.prototype.hasDrained = function hasDrained() {
    var self = this;

    if (self.pending.in === 0 &&
        self.pending.out === 0) {
        return true;
    } else if (self._isCollDrained(self.requests.in) &&
               self._isCollDrained(self.requests.out)) {
        return true;
    }

    return false;
};

Operations.prototype.checkDrained = function checkDrained() {
    var self = this;

    if (self.hasDrained()) {
        self.drainEvent.emit(self);
        self.drainEvent.removeAllListeners();
    }
};

Operations.prototype._isCollDrained = function _isCollDrained(coll) {
    var self = this;

    for (var id in coll) {
        var op = coll[id];
        if (!(op instanceof OperationTombstone) &&
            !op.drained && !(
                self.connection.channel.drainExempt &&
                self.connection.channel.drainExempt(op))
        ) {
            return false;
        }
    }

    return true;
};

Operations.prototype.popOutReq = function popOutReq(id, context) {
    var self = this;

    var req = self.requests.out[id];
    if (!req) {
        self.logMissingOutRequest(id, context);
        return null;
    } else if (req.isTombstone) {
        return null;
    }

    if (req.timeHeapHandle) {
        req.timeHeapHandle.cancel();
        req.timeHeapHandle = null;
    } else {
        self.logger.warn('Found OutRequest without timeHeapHandle', {
            serviceName: req.serviceName,
            endpoint: req.endpoint,
            socketRemoteAddr: req.remoteAddr,
            callerName: req.callerName
        });
    }

    var tombstone = new OperationTombstone(
        self, id, self.timers.now(), req, context
    );
    self.requests.out[id] = tombstone;
    tombstone.timeHeapHandle = self.connection.channel.timeHeap.update(tombstone, tombstone.time);

    var errors = 0;

    if (tombstone.isPendingError) {
        self.pending.errors++;
        errors++;
    }

    req.operations = null;
    self.pending.out--;
    if (self.draining) {
        self.checkDrained();
    }

    self.emitPendingChange(0, -1, errors);

    return req;
};

Operations.prototype.logMissingOutRequest =
function logMissingOutRequest(id, context) {
    var self = this;

    // context is err or res
    if (context && context.originalId) {
        context = {
            error: context,
            id: context.originalId,
            info: 'got error frame for unknown id'
        };
    } else if (context && context.id) {
        context = {
            responseId: context.id,
            code: context.code,
            arg1: Buffer.isBuffer(context.arg1) ?
                String(context.arg1) : 'streamed-arg1',
            info: 'got call response for unknown id'
        };
    }

    // This could be because of a confused / corrupted server.
    self.logger.info('popOutReq received for unknown or lost id', {
        context: context,
        socketRemoteAddr: self.connection.socketRemoteAddr,
        direction: self.connection.direction
    });
};

Operations.prototype.popInReq = function popInReq(id) {
    var self = this;

    var req = self.requests.in[id];
    if (!req) {
        // TODO warn ?
        return null;
    }

    if (req.timeHeapHandle) {
        req.timeHeapHandle.cancel();
        req.timeHeapHandle = null;
    }

    delete self.requests.in[id];
    self.pending.in--;

    self.emitPendingChange(-1, 0, 0);

    if (self.draining) {
        self.checkDrained();
    }

    return req;
};

Operations.prototype.clear = function clear() {
    var self = this;

    var now = self.timers.now();
    var inReqKeys = Object.keys(self.requests.in);
    var outReqKeys = Object.keys(self.requests.out);

    for (var i = 0; i < inReqKeys.length; i++) {
        self.popInReq(inReqKeys[i]);
    }
    for (var j = 0; j < outReqKeys.length; j++) {
        self.popOutReq(outReqKeys[j]);

        var tombstone = self.requests.out[outReqKeys[j]];
        if (tombstone.timeHeapHandle) {
            tombstone.timeHeapHandle.cancel();
        }
        tombstone.destroy(now);
    }
};

Operations.prototype.destroy = function destroy() {
    var self = this;

    self.destroyed = true;
};

Operations.prototype.sanitySweep = function sanitySweep(callback) {
    var self = this;

    self._sweepOps(self.requests.in, 'in', doneSweep);
    self._sweepOps(self.requests.out, 'out', doneSweep);

    var i = 0;
    function doneSweep() {
        i++;
        if (i === 2) {
            callback();
        }
    }
};

Operations.prototype._sweepOps = function _sweepOps(ops, direction, callback) {
    var self = this;

    // keep track of all pending change made during the sweep so we can
    // (maybe) emit one event at the end of the sweep

    self.pendingChangeDelta = {
        in: 0,
        out: 0,
        errors: 0
    };
    var pendingDirty = false;

    var now = self.timers.now();

    nextOp(Object.keys(ops), 0, callback);

    function nextOp(opKeys, i, done) {
        if (i >= opKeys.length) {
            done(null);
            return;
        }

        var id = opKeys[i];
        var op = ops[id];
        if (op === undefined) {
            self.logger.warn('unexpected undefined operation', {
                direction: direction,
                id: id
            });
        } else if (op.timedOut) {
            self.logger.warn('lingering timed-out operation', {
                direction: direction,
                id: id
            });

            if (direction === 'in') {
                self.popInReq(id);
                pendingDirty = true;
            } else if (direction === 'out') {
                self.popOutReq(id);
                pendingDirty = true;
            }
        } else if (op.isTombstone) {
            var heap = self.connection.channel.timeHeap;
            var expireTime = op.time + op.timeout;

            if (!op.operations) {
                self.logger.warn('zombie tombstone', op.extendLogInfo({
                    direction: direction,
                    opKey: id
                }));
                delete ops[id];
                if (op.isPendingError) {
                    self.pending.errors--;
                    self.pendingChangeDelta.errors--;
                    pendingDirty = true;
                }
                op.operations = null;
                op.timeHeapHandle.cancel();
                op.timeHeapHandle = null;
            } else if (expireTime < now && heap.lastRun > expireTime) {
                self.logger.warn('stale tombstone', op.extendLogInfo({
                    direction: direction,
                    opKey: id,
                    now: now,
                    staleDelta: op.time + op.timeout - now,
                    expireTime: expireTime,
                    heapLastRun: heap.lastRun
                }));
                delete ops[id];
                if (op.isPendingError) {
                    self.pending.errors--;
                    self.pendingChangeDelta.errors--;
                    pendingDirty = true;
                }
                op.operations = null;
                op.timeHeapHandle.cancel();
                op.timeHeapHandle = null;
            }
        }

        setImmediate(deferNextOp);

        function deferNextOp() {
            nextOp(opKeys, i + 1, done);
        }
    }

    if (pendingDirty) {
        self.pendingChangeEvent.emit(self, self.pendingChangeDelta);
    }
    self.pendingChangeDelta = null;
};
<commit_msg>linting: [operations] comply with no-undef rule<commit_after>// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

var errors = require('./errors.js');
var inherits = require('util').inherits;
var process = require('process');
var Buffer = require('buffer').Buffer;
var setImmediate = require('timers').setImmediate;
var EventEmitter = require('./lib/event_emitter');

var TOMBSTONE_TTL_OFFSET = 500;
var MAX_TOMBSTONE_TTL = 5000;

module.exports = Operations;

function Operations(opts) {
    var self = this;

    EventEmitter.call(self);
    self.draining = false;
    self.drainEvent = self.defineEvent('drain');
    self.pendingChangeEvent = self.defineEvent('pendingChange');
    self.pendingChangeDelta = null;

    self.timers = opts.timers;
    self.logger = opts.logger;
    self.random = opts.random;
    self.connectionStalePeriod = opts.connectionStalePeriod;
    self.maxTombstoneTTL = MAX_TOMBSTONE_TTL;

    self.connection = opts.connection;
    // TODO need this?
    self.destroyed = false;

    self.requests = {
        in: Object.create(null),
        out: Object.create(null)
    };
    self.pending = {
        in: 0,
        out: 0,
        errors: 0
    };
    self.lastTimeoutTime = 0;
}
inherits(Operations, EventEmitter);

Operations.prototype.emitPendingChange =
function emitPendingChange(numIn, numOut, numErrors) {
    var self = this;

    if (self.pendingChangeDelta !== null) {
        self.pendingChangeDelta.in += numIn;
        self.pendingChangeDelta.out += numOut;
        self.pendingChangeDelta.errors += numErrors;
    } else {
        self.pendingChangeEvent.emit(self, {
            in: numIn,
            out: numOut,
            errors: numErrors
        });
    }
};

Operations.prototype.extendLogInfo =
function extendLogInfo(info) {
    var self = this;

    if (self.connection) {
        info = self.connection.extendLogInfo(info);
    }

    return info;
};

Operations.prototype.setMaxTombstoneTTL =
function setMaxTombstoneTTL(ttl) {
    var self = this;

    self.maxTombstoneTTL = ttl;
};

function OperationTombstone(operations, id, time, req, context) {
    var self = this;

    var timeout = Math.min(
        TOMBSTONE_TTL_OFFSET + req.timeout,
        operations.maxTombstoneTTL
    );

    self.type = 'tchannel.operation.tombstone';
    self.isTombstone = true;
    self.logger = operations.logger;
    self.operations = operations;
    self.id = id;
    self.time = time;
    self.timeout = timeout;
    self.timeHeapHandle = null;
    self.destroyed = false;
    self.serviceName = req.serviceName;
    self.callerName = req.callerName;
    self.endpoint = req.endpoint;

    self.isPendingError = false;
    if (context && context.isErrorFrame) {
        self.isPendingError = errors.isPendingError(context.codeName);
    }
}

OperationTombstone.prototype.extendLogInfo = function extendLogInfo(info) {
    var self = this;

    info.id = self.id;
    info.serviceName = self.serviceName;
    info.callerName = self.callerName;
    info.endpoint = self.endpoint;
    info.tombstoneTime = self.time;
    info.tombstoneTTL = self.timeout;
    info.heapCanceled = self.timeHeapHandle && !self.timeHeapHandle.item;
    info.heapExpireTime = self.timeHeapHandle && self.timeHeapHandle.expireTime;
    info.heapAmItem = self.timeHeapHandle && self.timeHeapHandle.item === self;

    if (self.operations) {
        info = self.operations.extendLogInfo(info);
        var other = self.operations.requests.out[self.id];
        if (self !== other) {
            info.otherType = typeof other;
            info.otherConstructorName = other && other.constructor && other.constructor.name;
        }
    }

    return info;
};

OperationTombstone.prototype.destroy = function destroy(now) {
    var self = this;

    self.destroyed = true;

    self.onTimeout(now);
};

OperationTombstone.prototype.onTimeout = function onTimeout(now) {
    var self = this;

    if (!self.destroyed && now < self.timeout + self.time) {
        self.logger.error('tombstone timed out too early', self.extendLogInfo({
            now: now,
            expireTime: self.timeout + self.time,
            delta: (self.timeout + self.time) - now
        }));
    }

    if (self.operations &&
        self.operations.requests.out[self.id] === self) {
        delete self.operations.requests.out[self.id];
        if (self.isPendingError) {
            self.operations.pending.errors--;
            self.operations.emitPendingChange(0, 0, -1);
        }
        self.operations = null;
    } else {
        self.logger.warn('mismatched expired operation tombstone', self.extendLogInfo({}));
        self.operations = null;
    }

    self.timeHeapHandle = null;
};

Operations.prototype.checkLastTimeoutTime = function checkLastTimeoutTime(now) {
    var self = this;

    if (self.lastTimeoutTime &&
        now > self.lastTimeoutTime + self.connectionStalePeriod
    ) {
        self._deferResetDueToTimeouts(now);
    } else if (!self.lastTimeoutTime) {
        self.lastTimeoutTime = now;
    }
};

Operations.prototype._deferResetDueToTimeouts = function _deferResetDueToTimeouts(now) {
    var self = this;

    var elapsed = now - self.lastTimeoutTime;
    var err = errors.ConnectionStaleTimeoutError({
        period: self.connectionStalePeriod,
        elapsed: elapsed,
        lastTimeoutTime: self.lastTimeoutTime
    });
    process.nextTick(opCheckLastTimedout);

    function opCheckLastTimedout() {
        self.logger.warn('destroying socket from timeouts', self.connection.extendLogInfo({
            error: err
        }));
        self.connection.resetAll(err);
    }
};

Operations.prototype.getRequests = function getRequests() {
    var self = this;

    return self.requests;
};

Operations.prototype.getPending = function getPending() {
    var self = this;

    return self.pending;
};

// TODO: Merge getOutTombstone() with getOutReq()
Operations.prototype.getOutTombstone = function getOutTombstone(id) {
    var self = this;

    var op = self.requests.out[id] || null;
    if (op && !op.isTombstone) {
        return null;
    }

    return op;
};

Operations.prototype.getOutReq = function getOutReq(id) {
    var self = this;

    var req = self.requests.out[id] || null;
    if (req && req.isTombstone) {
        return null;
    }

    return req;
};

Operations.prototype.getInReq = function getInReq(id) {
    var self = this;

    return self.requests.in[id];
};

Operations.prototype.addOutReq = function addOutReq(req) {
    var self = this;

    req.operations = self;
    self.requests.out[req.id] = req;
    self.pending.out++;

    req.timeHeapHandle = self.connection.channel.timeHeap.update(req);

    self.emitPendingChange(0, 1, 0);

    return req;
};

Operations.prototype.addInReq = function addInReq(req) {
    var self = this;

    req.operations = self;
    self.requests.in[req.id] = req;
    self.pending.in++;

    req.timeHeapHandle = self.connection.channel.timeHeap.update(req);

    self.emitPendingChange(1, 0, 0);

    return req;
};

Operations.prototype.hasDrained = function hasDrained() {
    var self = this;

    if (self.pending.in === 0 &&
        self.pending.out === 0) {
        return true;
    } else if (self._isCollDrained(self.requests.in) &&
               self._isCollDrained(self.requests.out)) {
        return true;
    }

    return false;
};

Operations.prototype.checkDrained = function checkDrained() {
    var self = this;

    if (self.hasDrained()) {
        self.drainEvent.emit(self);
        self.drainEvent.removeAllListeners();
    }
};

Operations.prototype._isCollDrained = function _isCollDrained(coll) {
    var self = this;

    for (var id in coll) {
        var op = coll[id];
        if (!(op instanceof OperationTombstone) &&
            !op.drained && !(
                self.connection.channel.drainExempt &&
                self.connection.channel.drainExempt(op))
        ) {
            return false;
        }
    }

    return true;
};

Operations.prototype.popOutReq = function popOutReq(id, context) {
    var self = this;

    var req = self.requests.out[id];
    if (!req) {
        self.logMissingOutRequest(id, context);
        return null;
    } else if (req.isTombstone) {
        return null;
    }

    if (req.timeHeapHandle) {
        req.timeHeapHandle.cancel();
        req.timeHeapHandle = null;
    } else {
        self.logger.warn('Found OutRequest without timeHeapHandle', {
            serviceName: req.serviceName,
            endpoint: req.endpoint,
            socketRemoteAddr: req.remoteAddr,
            callerName: req.callerName
        });
    }

    var tombstone = new OperationTombstone(
        self, id, self.timers.now(), req, context
    );
    self.requests.out[id] = tombstone;
    tombstone.timeHeapHandle = self.connection.channel.timeHeap.update(tombstone, tombstone.time);

    var errors = 0;

    if (tombstone.isPendingError) {
        self.pending.errors++;
        errors++;
    }

    req.operations = null;
    self.pending.out--;
    if (self.draining) {
        self.checkDrained();
    }

    self.emitPendingChange(0, -1, errors);

    return req;
};

Operations.prototype.logMissingOutRequest =
function logMissingOutRequest(id, context) {
    var self = this;

    // context is err or res
    if (context && context.originalId) {
        context = {
            error: context,
            id: context.originalId,
            info: 'got error frame for unknown id'
        };
    } else if (context && context.id) {
        context = {
            responseId: context.id,
            code: context.code,
            arg1: Buffer.isBuffer(context.arg1) ?
                String(context.arg1) : 'streamed-arg1',
            info: 'got call response for unknown id'
        };
    }

    // This could be because of a confused / corrupted server.
    self.logger.info('popOutReq received for unknown or lost id', {
        context: context,
        socketRemoteAddr: self.connection.socketRemoteAddr,
        direction: self.connection.direction
    });
};

Operations.prototype.popInReq = function popInReq(id) {
    var self = this;

    var req = self.requests.in[id];
    if (!req) {
        // TODO warn ?
        return null;
    }

    if (req.timeHeapHandle) {
        req.timeHeapHandle.cancel();
        req.timeHeapHandle = null;
    }

    delete self.requests.in[id];
    self.pending.in--;

    self.emitPendingChange(-1, 0, 0);

    if (self.draining) {
        self.checkDrained();
    }

    return req;
};

Operations.prototype.clear = function clear() {
    var self = this;

    var now = self.timers.now();
    var inReqKeys = Object.keys(self.requests.in);
    var outReqKeys = Object.keys(self.requests.out);

    for (var i = 0; i < inReqKeys.length; i++) {
        self.popInReq(inReqKeys[i]);
    }
    for (var j = 0; j < outReqKeys.length; j++) {
        self.popOutReq(outReqKeys[j]);

        var tombstone = self.requests.out[outReqKeys[j]];
        if (tombstone.timeHeapHandle) {
            tombstone.timeHeapHandle.cancel();
        }
        tombstone.destroy(now);
    }
};

Operations.prototype.destroy = function destroy() {
    var self = this;

    self.destroyed = true;
};

Operations.prototype.sanitySweep = function sanitySweep(callback) {
    var self = this;

    self._sweepOps(self.requests.in, 'in', doneSweep);
    self._sweepOps(self.requests.out, 'out', doneSweep);

    var i = 0;
    function doneSweep() {
        i++;
        if (i === 2) {
            callback();
        }
    }
};

Operations.prototype._sweepOps = function _sweepOps(ops, direction, callback) {
    var self = this;

    // keep track of all pending change made during the sweep so we can
    // (maybe) emit one event at the end of the sweep

    self.pendingChangeDelta = {
        in: 0,
        out: 0,
        errors: 0
    };
    var pendingDirty = false;

    var now = self.timers.now();

    nextOp(Object.keys(ops), 0, callback);

    function nextOp(opKeys, i, done) {
        if (i >= opKeys.length) {
            done(null);
            return;
        }

        var id = opKeys[i];
        var op = ops[id];
        if (op === undefined) {
            self.logger.warn('unexpected undefined operation', {
                direction: direction,
                id: id
            });
        } else if (op.timedOut) {
            self.logger.warn('lingering timed-out operation', {
                direction: direction,
                id: id
            });

            if (direction === 'in') {
                self.popInReq(id);
                pendingDirty = true;
            } else if (direction === 'out') {
                self.popOutReq(id);
                pendingDirty = true;
            }
        } else if (op.isTombstone) {
            var heap = self.connection.channel.timeHeap;
            var expireTime = op.time + op.timeout;

            if (!op.operations) {
                self.logger.warn('zombie tombstone', op.extendLogInfo({
                    direction: direction,
                    opKey: id
                }));
                delete ops[id];
                if (op.isPendingError) {
                    self.pending.errors--;
                    self.pendingChangeDelta.errors--;
                    pendingDirty = true;
                }
                op.operations = null;
                op.timeHeapHandle.cancel();
                op.timeHeapHandle = null;
            } else if (expireTime < now && heap.lastRun > expireTime) {
                self.logger.warn('stale tombstone', op.extendLogInfo({
                    direction: direction,
                    opKey: id,
                    now: now,
                    staleDelta: op.time + op.timeout - now,
                    expireTime: expireTime,
                    heapLastRun: heap.lastRun
                }));
                delete ops[id];
                if (op.isPendingError) {
                    self.pending.errors--;
                    self.pendingChangeDelta.errors--;
                    pendingDirty = true;
                }
                op.operations = null;
                op.timeHeapHandle.cancel();
                op.timeHeapHandle = null;
            }
        }

        setImmediate(deferNextOp);

        function deferNextOp() {
            nextOp(opKeys, i + 1, done);
        }
    }

    if (pendingDirty) {
        self.pendingChangeEvent.emit(self, self.pendingChangeDelta);
    }
    self.pendingChangeDelta = null;
};
<|endoftext|>