<commit_before>angular.module("umbraco.directives")
    .directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) {
        return {
            scope: {
                uniqueId: '=',
                value: '=',
                onClick: '&',
                onFocus: '&',
                onBlur: '&',
                configuration:"=",
                onMediaPickerClick: "=",
                onEmbedClick: "=",
                onMacroPickerClick: "=",
                onLinkPickerClick: "="
            },
            template: "<textarea ng-model=\"value\" rows=\"10\" class=\"mceNoEditor\" style=\"overflow:hidden\" id=\"{{uniqueId}}\"></textarea>",
            replace: true,
            link: function (scope, element, attrs) {

                var initTiny = function () {

                    //we always fetch the default one, and then override parts with our own
                    tinyMceService.configuration().then(function (tinyMceConfig) {

                        //config value from general tinymce.config file
                        var validElements = tinyMceConfig.validElements;
                        var fallbackStyles = [{title: "Page header", block: "h2"}, {title: "Section header", block: "h3"}, {title: "Paragraph header", block: "h4"}, {title: "Normal", block: "p"}, {title: "Quote", block: "blockquote"}, {title: "Code", block: "code"}];

                        //These are absolutely required in order for the macros to render inline
                        //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
                        var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]";

                        var invalidElements = tinyMceConfig.inValidElements;
                        var plugins = _.map(tinyMceConfig.plugins, function (plugin) {
                            if (plugin.useOnFrontend) {
                                return plugin.name;
                            }
                        }).join(" ") + " autoresize";

                        //config value on the data type
                        var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | ");
                        var stylesheets = [];

                        var styleFormats = [];
                        var await = [];

                        //queue file loading
                        if (typeof (tinymce) === "undefined") {
                                await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope));
                        }

                        if(scope.configuration && scope.configuration.toolbar){
                            toolbar = scope.configuration.toolbar.join(' | ');
                        }

                        if(scope.configuration && scope.configuration.stylesheets){
                            angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){

                                    stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css");
                                    await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) {
                                        angular.forEach(rules, function (rule) {
                                          var r = {};
                                          r.title = rule.name;
                                          if (rule.selector[0] === ".") {
                                              r.inline = "span";
                                              r.classes = rule.selector.substring(1);
                                          }else if (rule.selector[0] === "#") {
                                              //Even though this will render in the style drop down, it will not actually be applied
                                              // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense
                                              // since only one element can have one id.
                                              r.inline = "span";
                                              r.attributes = { id: rule.selector.substring(1) };
                                          }else if (rule.selector[0] != "." && rule.selector.indexOf(".") > -1) {
                                              var split = rule.selector.split(".");
                                              r.block = split[0];
                                              r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " ");
                                          }else if (rule.selector[0] != "#" && rule.selector.indexOf("#") > -1) {
                                              var split = rule.selector.split("#");
                                              r.block = split[0];
                                              r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1);
                                          }else {
                                              r.block = rule.selector;
                                          }
                                          styleFormats.push(r);
                                        });
                                    }));
                            });
                        }else{
                            stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css");
                            styleFormats = fallbackStyles;
                        }

                        //stores a reference to the editor
                        var tinyMceEditor = null;
                        $q.all(await).then(function () {

                            var uniqueId = scope.uniqueId;

                            //create a baseline Config to exten upon
                            var baseLineConfigObj = {
                                mode: "exact",
                                skin: "umbraco",
                                plugins: plugins,
                                valid_elements: validElements,
                                invalid_elements: invalidElements,
                                extended_valid_elements: extendedValidElements,
                                menubar: false,
                                statusbar: false,
                                relative_urls: false,
                                toolbar: toolbar,
                                content_css: stylesheets,
                                style_formats: styleFormats,
                                autoresize_bottom_margin: 0
                            };

                            if (tinyMceConfig.customConfig) {

                                //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to
                                // convert it to json instead of having it as a string since this is what tinymce requires
                                for (var i in tinyMceConfig.customConfig) {
                                    var val = tinyMceConfig.customConfig[i];
                                    if (val) {
                                        val = val.toString().trim();
                                        if (val.detectIsJson()) {
                                            try {
                                                tinyMceConfig.customConfig[i] = JSON.parse(val);
                                                //now we need to check if this custom config key is defined in our baseline, if it is we don't want to
                                                //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise
                                                //if it's an object it will overwrite the baseline
                                                if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) {
                                                    //concat it and below this concat'd array will overwrite the baseline in angular.extend
                                                    tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]);
                                                }
                                            }
                                            catch (e) {
                                                //cannot parse, we'll just leave it
                                            }
                                        }
                                    }
                                }

                                angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
                            }

                            //set all the things that user configs should not be able to override
                            baseLineConfigObj.elements = uniqueId;
                            baseLineConfigObj.setup = function (editor) {

                                //set the reference
                                tinyMceEditor = editor;

                                //enable browser based spell checking
                                editor.on('init', function (e) {

                                    editor.getBody().setAttribute('spellcheck', true);

                                    //force overflow to hidden to prevent no needed scroll
                                    editor.getBody().style.overflow = "hidden";

                                    $timeout(function(){
                                        if(scope.value === null){
                                            editor.focus();
                                        }
                                    }, 400);

                                });

                                //when we leave the editor (maybe)
                                editor.on('blur', function (e) {
                                    editor.save();
                                    angularHelper.safeApply(scope, function () {
                                        scope.value = editor.getContent();

                                        var _toolbar = $(editor.editorContainer)
                                             .find(".mce-toolbar");

                                        if(scope.onBlur){
                                            scope.onBlur();
                                        }

                                    });
                                });

                                // Focus on editor
                                editor.on('focus', function (e) {
                                    angularHelper.safeApply(scope, function () {

                                        if(scope.onFocus){
                                            scope.onFocus();
                                        }

                                    });
                                });

                                // Click on editor
                                editor.on('click', function (e) {
                                    angularHelper.safeApply(scope, function () {

                                        if(scope.onClick){
                                            scope.onClick();
                                        }

                                    });
                                });

                                //when buttons modify content
                                editor.on('ExecCommand', function (e) {
                                    editor.save();
                                    angularHelper.safeApply(scope, function () {
                                        scope.value = editor.getContent();
                                    });
                                });

                                // Update model on keypress
                                editor.on('KeyUp', function (e) {
                                    editor.save();
                                    angularHelper.safeApply(scope, function () {
                                        scope.value = editor.getContent();
                                    });
                                });

                                // Update model on change, i.e. copy/pasted text, plugins altering content
                                editor.on('SetContent', function (e) {
                                    if (!e.initial) {
                                        editor.save();
                                        angularHelper.safeApply(scope, function () {
                                            scope.value = editor.getContent();
                                        });
                                    }
                                });

                                editor.on('ObjectResized', function (e) {
                                    var qs = "?width=" + e.width + "&height=" + e.height;
                                    var srcAttr = $(e.target).attr("src");
                                    var path = srcAttr.split("?")[0];
                                    $(e.target).attr("data-mce-src", path + qs);
                                });

                                //Create the insert link plugin
                                tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){
                                    if(scope.onLinkPickerClick) {
                                        scope.onLinkPickerClick(editor, currentTarget, anchorElement);
                                    }
                                });

                                //Create the insert media plugin
                                tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){
                                    if(scope.onMediaPickerClick) {
                                        scope.onMediaPickerClick(editor, currentTarget, userData);
                                    }
                                });

                                //Create the embedded plugin
                                tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){
                                    if(scope.onEmbedClick) {
                                        scope.onEmbedClick(editor);
                                    }
                                });

                                //Create the insert macro plugin
                                tinyMceService.createInsertMacro(editor, scope, function(dialogData){
                                    if(scope.onMacroPickerClick) {
                                        scope.onMacroPickerClick(editor, dialogData);
                                    }
                                });

                            };

                            /** Loads in the editor */
                            function loadTinyMce() {

                                //we need to add a timeout here, to force a redraw so TinyMCE can find
                                //the elements needed
                                $timeout(function () {
                                    tinymce.DOM.events.domLoaded = true;
                                    tinymce.init(baseLineConfigObj);
                                }, 150, false);
                            }

                            loadTinyMce();

                            //here we declare a special method which will be called whenever the value has changed from the server
                            //this is instead of doing a watch on the model.value = faster
                            //scope.model.onValueChanged = function (newVal, oldVal) {
                            //    //update the display val again if it has changed from the server;
                            //    tinyMceEditor.setContent(newVal, { format: 'raw' });
                            //    //we need to manually fire this event since it is only ever fired based on loading from the DOM, this
                            //    // is required for our plugins listening to this event to execute
                            //    tinyMceEditor.fire('LoadContent', null);
                            //};

                            //listen for formSubmitting event (the result is callback used to remove the event subscription)
                            var unsubscribe = scope.$on("formSubmitting", function () {
                                //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer
                                // we do parse it out on the server side but would be nice to do that on the client side before as well.
                                scope.value = tinyMceEditor.getContent();
                            });

                            //when the element is disposed we need to unsubscribe!
                            // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
                            // element might still be there even after the modal has been hidden.
                            scope.$on('$destroy', function () {
                                unsubscribe();
                            });

                        });

                    });

                };

                initTiny();

            }
        };
    });
<commit_msg>Fix js errors for grunt to build<commit_after>angular.module("umbraco.directives")
    .directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) {
        return {
            scope: {
                uniqueId: '=',
                value: '=',
                onClick: '&',
                onFocus: '&',
                onBlur: '&',
                configuration:"=",
                onMediaPickerClick: "=",
                onEmbedClick: "=",
                onMacroPickerClick: "=",
                onLinkPickerClick: "="
            },
            template: "<textarea ng-model=\"value\" rows=\"10\" class=\"mceNoEditor\" style=\"overflow:hidden\" id=\"{{uniqueId}}\"></textarea>",
            replace: true,
            link: function (scope, element, attrs) {

                var initTiny = function () {

                    //we always fetch the default one, and then override parts with our own
                    tinyMceService.configuration().then(function (tinyMceConfig) {

                        //config value from general tinymce.config file
                        var validElements = tinyMceConfig.validElements;
                        var fallbackStyles = [{title: "Page header", block: "h2"}, {title: "Section header", block: "h3"}, {title: "Paragraph header", block: "h4"}, {title: "Normal", block: "p"}, {title: "Quote", block: "blockquote"}, {title: "Code", block: "code"}];

                        //These are absolutely required in order for the macros to render inline
                        //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
                        var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]";

                        var invalidElements = tinyMceConfig.inValidElements;
                        var plugins = _.map(tinyMceConfig.plugins, function (plugin) {
                            if (plugin.useOnFrontend) {
                                return plugin.name;
                            }
                        }).join(" ") + " autoresize";

                        //config value on the data type
                        var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | ");
                        var stylesheets = [];

                        var styleFormats = [];
                        var await = [];

                        //queue file loading
                        if (typeof (tinymce) === "undefined") {
                                await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope));
                        }

                        if(scope.configuration && scope.configuration.toolbar){
                            toolbar = scope.configuration.toolbar.join(' | ');
                        }

                        if(scope.configuration && scope.configuration.stylesheets){
                            angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){

                                    stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css");
                                    await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) {
                                        angular.forEach(rules, function (rule) {
                                          var r = {};
                                          var split = "";
                                          r.title = rule.name;
                                          if (rule.selector[0] === ".") {
                                              r.inline = "span";
                                              r.classes = rule.selector.substring(1);
                                          }else if (rule.selector[0] === "#") {
                                              //Even though this will render in the style drop down, it will not actually be applied
                                              // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense
                                              // since only one element can have one id.
                                              r.inline = "span";
                                              r.attributes = { id: rule.selector.substring(1) };
                                          }else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) {
                                              split = rule.selector.split(".");
                                              r.block = split[0];
                                              r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " ");
                                          }else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) {
                                              split = rule.selector.split("#");
                                              r.block = split[0];
                                              r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1);
                                          }else {
                                              r.block = rule.selector;
                                          }
                                          styleFormats.push(r);
                                        });
                                    }));
                            });
                        }else{
                            stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css");
                            styleFormats = fallbackStyles;
                        }

                        //stores a reference to the editor
                        var tinyMceEditor = null;
                        $q.all(await).then(function () {

                            var uniqueId = scope.uniqueId;

                            //create a baseline Config to exten upon
                            var baseLineConfigObj = {
                                mode: "exact",
                                skin: "umbraco",
                                plugins: plugins,
                                valid_elements: validElements,
                                invalid_elements: invalidElements,
                                extended_valid_elements: extendedValidElements,
                                menubar: false,
                                statusbar: false,
                                relative_urls: false,
                                toolbar: toolbar,
                                content_css: stylesheets,
                                style_formats: styleFormats,
                                autoresize_bottom_margin: 0
                            };

                            if (tinyMceConfig.customConfig) {

                                //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to
                                // convert it to json instead of having it as a string since this is what tinymce requires
                                for (var i in tinyMceConfig.customConfig) {
                                    var val = tinyMceConfig.customConfig[i];
                                    if (val) {
                                        val = val.toString().trim();
                                        if (val.detectIsJson()) {
                                            try {
                                                tinyMceConfig.customConfig[i] = JSON.parse(val);
                                                //now we need to check if this custom config key is defined in our baseline, if it is we don't want to
                                                //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise
                                                //if it's an object it will overwrite the baseline
                                                if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) {
                                                    //concat it and below this concat'd array will overwrite the baseline in angular.extend
                                                    tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]);
                                                }
                                            }
                                            catch (e) {
                                                //cannot parse, we'll just leave it
                                            }
                                        }
                                    }
                                }

                                angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
                            }

                            //set all the things that user configs should not be able to override
                            baseLineConfigObj.elements = uniqueId;
                            baseLineConfigObj.setup = function (editor) {

                                //set the reference
                                tinyMceEditor = editor;

                                //enable browser based spell checking
                                editor.on('init', function (e) {

                                    editor.getBody().setAttribute('spellcheck', true);

                                    //force overflow to hidden to prevent no needed scroll
                                    editor.getBody().style.overflow = "hidden";

                                    $timeout(function(){
                                        if(scope.value === null){
                                            editor.focus();
                                        }
                                    }, 400);

                                });

                                //when we leave the editor (maybe)
                                editor.on('blur', function (e) {
                                    editor.save();
                                    angularHelper.safeApply(scope, function () {
                                        scope.value = editor.getContent();

                                        var _toolbar = $(editor.editorContainer)
                                             .find(".mce-toolbar");

                                        if(scope.onBlur){
                                            scope.onBlur();
                                        }

                                    });
                                });

                                // Focus on editor
                                editor.on('focus', function (e) {
                                    angularHelper.safeApply(scope, function () {

                                        if(scope.onFocus){
                                            scope.onFocus();
                                        }

                                    });
                                });

                                // Click on editor
                                editor.on('click', function (e) {
                                    angularHelper.safeApply(scope, function () {

                                        if(scope.onClick){
                                            scope.onClick();
                                        }

                                    });
                                });

                                //when buttons modify content
                                editor.on('ExecCommand', function (e) {
                                    editor.save();
                                    angularHelper.safeApply(scope, function () {
                                        scope.value = editor.getContent();
                                    });
                                });

                                // Update model on keypress
                                editor.on('KeyUp', function (e) {
                                    editor.save();
                                    angularHelper.safeApply(scope, function () {
                                        scope.value = editor.getContent();
                                    });
                                });

                                // Update model on change, i.e. copy/pasted text, plugins altering content
                                editor.on('SetContent', function (e) {
                                    if (!e.initial) {
                                        editor.save();
                                        angularHelper.safeApply(scope, function () {
                                            scope.value = editor.getContent();
                                        });
                                    }
                                });

                                editor.on('ObjectResized', function (e) {
                                    var qs = "?width=" + e.width + "&height=" + e.height;
                                    var srcAttr = $(e.target).attr("src");
                                    var path = srcAttr.split("?")[0];
                                    $(e.target).attr("data-mce-src", path + qs);
                                });

                                //Create the insert link plugin
                                tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){
                                    if(scope.onLinkPickerClick) {
                                        scope.onLinkPickerClick(editor, currentTarget, anchorElement);
                                    }
                                });

                                //Create the insert media plugin
                                tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){
                                    if(scope.onMediaPickerClick) {
                                        scope.onMediaPickerClick(editor, currentTarget, userData);
                                    }
                                });

                                //Create the embedded plugin
                                tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){
                                    if(scope.onEmbedClick) {
                                        scope.onEmbedClick(editor);
                                    }
                                });

                                //Create the insert macro plugin
                                tinyMceService.createInsertMacro(editor, scope, function(dialogData){
                                    if(scope.onMacroPickerClick) {
                                        scope.onMacroPickerClick(editor, dialogData);
                                    }
                                });

                            };

                            /** Loads in the editor */
                            function loadTinyMce() {

                                //we need to add a timeout here, to force a redraw so TinyMCE can find
                                //the elements needed
                                $timeout(function () {
                                    tinymce.DOM.events.domLoaded = true;
                                    tinymce.init(baseLineConfigObj);
                                }, 150, false);
                            }

                            loadTinyMce();

                            //here we declare a special method which will be called whenever the value has changed from the server
                            //this is instead of doing a watch on the model.value = faster
                            //scope.model.onValueChanged = function (newVal, oldVal) {
                            //    //update the display val again if it has changed from the server;
                            //    tinyMceEditor.setContent(newVal, { format: 'raw' });
                            //    //we need to manually fire this event since it is only ever fired based on loading from the DOM, this
                            //    // is required for our plugins listening to this event to execute
                            //    tinyMceEditor.fire('LoadContent', null);
                            //};

                            //listen for formSubmitting event (the result is callback used to remove the event subscription)
                            var unsubscribe = scope.$on("formSubmitting", function () {
                                //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer
                                // we do parse it out on the server side but would be nice to do that on the client side before as well.
                                scope.value = tinyMceEditor.getContent();
                            });

                            //when the element is disposed we need to unsubscribe!
                            // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
                            // element might still be there even after the modal has been hidden.
                            scope.$on('$destroy', function () {
                                unsubscribe();
                            });

                        });

                    });

                };

                initTiny();

            }
        };
    });
<|endoftext|>


<commit_before>var http = require('http');
var fs = require('fs');
var url = require('url');

http.createServer(function (req, res) {
    var libPath = 'public/lib/';
    var q = url.parse(req.url, true).query;
    var fileRequest = req.url.match(/\/(.*\.js)/);

    console.log("request: url %s, page # %s", req.url, q.page);

    if (fileRequest) {
        var fileName = fileRequest[1];

        try {
            fileStats = fs.lstatSync(libPath + fileName);
            res.writeHead(200, {'Content-Type': 'application/json'});
            res.end(fs.readFileSync(libPath + fileName));
        }
        catch (e) {
            console.log(e);
        }
    }

    // the whole db
    if (req.url == '/books') {
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(fs.readFileSync('./public/db.json'));
    }

    // specific page numbers (with 4 being a 404) and index.html otherwise
    if (q.page == '1') {
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(fs.readFileSync('./public/p1.js'));
    }
    else if (q.page == '2') {
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(fs.readFileSync('./public/p2.js'));
    }
    else if (q.page == '3') {
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(fs.readFileSync('./public/p3.js'));
    }
    else if (q.page == '4') {
        res.writeHead(404, {'Content-Type': 'text/html'});
        res.end("404 :(");
    }
    else {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.end(fs.readFileSync('./public/index.html'));
    }
}).listen(3000);
<commit_msg>removing the old page-specific if statements, the whole point of this app is to automatically paginate results<commit_after>// imports
var http = require('http');
var fs = require('fs');
var url = require('url');

// script options
var libPath = './';

console.log("starting server...");

http.createServer(function (req, res) {
    var q = url.parse(req.url, true).query;
    var fileRequest = req.url.match(/\/(.*\.json$)/);

    console.log("request: url %s, page # %s", req.url, q.page);

    /*
     * remember: fileRequest is false if it doesn't match the json regex
     */

    if (fileRequest) {
        var fileName = fileRequest[1];

        try {
            fileStats = fs.lstatSync(libPath + fileName);
            res.writeHead(200, {'Content-Type': 'application/json'});
            res.end(fs.readFileSync(libPath + fileName));
        }
        catch (e) {
            console.log(e);
        }
    }
    else {
        res.writeHead(200, {'Content-Type': 'text/html'});
        res.end(fs.readFileSync('./index.html'));
    }
}).listen(3000);
<|endoftext|>


<commit_before>'use strict';

angular.module('shoppingList', ['core.shoppingList']).
    component('shoppingList', {
        template: require('./shopping-list.html'),
        // templateUrl: 'components/shopping-list/shopping-list.html',
        controllerAs: 'shoppingListCtrl',
        controller: ['ShoppingList', '$window',
            function shoppingListController(ShoppingList, $window) {
                var vm = this;
                vm.items = [];
                vm.itemsPool = [];
                vm.removeCount = 0;
                vm.empty = false;
                vm.currentInput = '';
                vm.newItem = '';
                vm.activateEdit = activateEdit;
                vm.deactivateEdit = deactivateEdit;
                vm.toggleView = toggleView;
                vm.editItem = editItem;
                vm.addItem = addItem;
                vm.checkIfRemoved = checkIfRemoved;
                vm.removeFromPool = removeFromPool;
                vm.removeItems = removeItems;

                ShoppingList.getItems().
                then(function(res) {
                    vm.items = res.data;
                    vm.itemsPool = JSON.parse(angular.toJson(res.data));
                    if (vm.items.length === 0) {
                        vm.empty = true;
                    }
                });

                function activateEdit(id) {
                    vm.currentInput = id;
                }

                function deactivateEdit() {
                    vm.currentInput = '';
                }

                function toggleView(id) {
                    // prevent other li's from showing edit
                    if (vm.currentInput === id) {
                        return true;
                    }
                    return false;
                }

                function editItem(item) {
                    ShoppingList.editItem({ item: item, id: vm.currentInput }).
                    then(function(res) {
                        vm.items = res.data;
                        vm.currentInput = '';
                    });
                }

                function addItem(item) {
                    if (item === '') {
                        return;
                    }

                    item = Array(item);

                    ShoppingList.saveItems(item).
                    then(function(res) {
                        vm.items = res.data;
                        vm.itemsPool = JSON.parse(angular.toJson(res.data));
                        vm.newItem = '';
                    });
                }

                function checkIfRemoved(item) {
                    var index = findIndex(vm.itemsPool, item);

                    if (index === -1) {
                        return 'deleted';
                    } else {
                        return '';
                    }
                }

                function removeFromPool(item) {
                    var index = findIndex(vm.itemsPool, item);

                    if (index >= 0) {
                        vm.itemsPool.splice(index, 1);
                        vm.removeCount++;
                    } else if (index === -1) {
                        vm.itemsPool.push(item);
                        vm.removeCount--;
                    }
                }

                function removeItems(items) {
                    ShoppingList.removeItem(items).
                    then(function(res) {
                        vm.items = res.data;
                        vm.removeCount = 0;
                        if (vm.items.length === 0) {
                            vm.empty = true;
                        }
                    });
                }

                // helper

                function findIndex(array, item) {
                    for (var i = 0; i < array.length; i++) {
                        if (array[i].id === item.id) {
                            return i;
                        }
                    }
                    return -1;
                }
            }
        ]
    });
<commit_msg>hide double click to edit<commit_after>'use strict';

angular.module('shoppingList', ['core.shoppingList']).
    component('shoppingList', {
        template: require('./shopping-list.html'),
        // templateUrl: 'components/shopping-list/shopping-list.html',
        controllerAs: 'shoppingListCtrl',
        controller: ['ShoppingList', '$window',
            function shoppingListController(ShoppingList, $window) {
                var vm = this;
                vm.items = [];
                vm.itemsPool = [];
                vm.removeCount = 0;
                vm.empty = false;
                vm.currentInput = '';
                vm.newItem = '';
                vm.activateEdit = activateEdit;
                vm.deactivateEdit = deactivateEdit;
                vm.toggleView = toggleView;
                vm.editItem = editItem;
                vm.addItem = addItem;
                vm.checkIfRemoved = checkIfRemoved;
                vm.removeFromPool = removeFromPool;
                vm.removeItems = removeItems;

                ShoppingList.getItems().
                then(function(res) {
                    vm.items = res.data;
                    vm.itemsPool = JSON.parse(angular.toJson(res.data));
                    if (vm.items.length === 0) {
                        vm.empty = true;
                    }
                });

                function activateEdit(id) {
                    vm.currentInput = id;
                }

                function deactivateEdit() {
                    vm.currentInput = '';
                }

                function toggleView(id) {
                    // prevent other li's from showing edit
                    if (vm.currentInput === id) {
                        return true;
                    }
                    return false;
                }

                function editItem(item) {
                    ShoppingList.editItem({ item: item, id: vm.currentInput }).
                    then(function(res) {
                        vm.items = res.data;
                        vm.currentInput = '';
                    });
                }

                function addItem(item) {
                    if (item === '') {
                        return;
                    }

                    item = Array(item);

                    ShoppingList.saveItems(item).
                    then(function(res) {
                        vm.empty = false;
                        vm.items = res.data;
                        vm.itemsPool = JSON.parse(angular.toJson(res.data));
                        vm.newItem = '';
                    });
                }

                function checkIfRemoved(item) {
                    var index = findIndex(vm.itemsPool, item);

                    if (index === -1) {
                        return 'deleted';
                    } else {
                        return '';
                    }
                }

                function removeFromPool(item) {
                    var index = findIndex(vm.itemsPool, item);

                    if (index >= 0) {
                        vm.itemsPool.splice(index, 1);
                        vm.removeCount++;
                    } else if (index === -1) {
                        vm.itemsPool.push(item);
                        vm.removeCount--;
                    }
                }

                function removeItems(items) {
                    ShoppingList.removeItem(items).
                    then(function(res) {
                        vm.items = res.data;
                        vm.removeCount = 0;
                        if (vm.items.length === 0) {
                            vm.empty = true;
                        }
                    });
                }

                // helper

                function findIndex(array, item) {
                    for (var i = 0; i < array.length; i++) {
                        if (array[i].id === item.id) {
                            return i;
                        }
                    }
                    return -1;
                }
            }
        ]
    });
<|endoftext|>


<commit_before>/*---------- DEPENDENCIES ----------*/

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const jsonParser = bodyParser.json();
const knex = require('./backend/database/connect')

/*--------- EXPRESS CONFIG ----------*/

// Serve the static frontend files
app.use(express.static('./build'));

// Use JSON Parser everywhere
app.use(jsonParser);

// CORs handling
app.use((request, response, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  next();
});

/*---------- DATABASE QUERIES ----------*/

// Select all medication entries from medication table
app.get('/medication', jsonParser, (request, response) => {
	knex.select()
        .from('messages')
        .where({ question_id: questionID })
        .returning('message_text', 'user_name', 'when_sent')
        .orderBy('when_sent')
        .then((messages) => {
            resolve({ messages: messages });
        }).catch((err) => {
            reject(err);
        });
)};

// Select the details of a specific medication by id
app.get('/medication/:id', jsonParser, (request, response) => {

});

// Select all past doses from dose_history table
app.get('/medication/history', jsonParser, (request, response) => {

})

// Add a new medication to the medication table
app.post('/medication/new', jsonParser, (request, response) => {
	let name = request.body.name;
	let dosage = request.body.dosage;
	let numDoses = request.body.numDoses;
	let firstDose = request.body.firstDose;
	let instructions = request.body.instructions;
	let precautions = request.body.precautions;

	let frequency = Math.floor(24 / numDoses);
	frequecy = 3600 * frequency;
	firstDose = 3600 * firstDose;
	let time = new Date().getTime() / 1000;
	let nextDoseSecs = firstDose + time;
	let nextDoseDate = new Date(0);
	nextDoseDate.setUTCSeconds(nextDose);

	knex.insert({ name: name, dosage: dosage, num_doses: numDoses, frequency: frequency, next_dose_secs: nextDoseSecs, next_dose_date: nextDoseDate, instructions: instrucstions, precautions: precautions })
        .returning(['name', 'dosage', 'num_doses', 'next_dose_secs', 'next_dose_date', 'instructions', 'precautions'])
        .into('medication')
        .then((data) => {
            return response.status(201).json({
            	name: data[0].name, dosage: data[0].dosage, numDoses: data[0].num_doses, nextDoseSecs: data[0].next_dose_secs, nextDoseDate: data[0].next_dose_date, instructions: data[0].instrucstion, precautions: data[0].precautions
            });
        })
        .catch((error) => {
            response.status(500).json({'error: ' error});
        });
});

// Edit the details of an existing medication
app.put('/medication/update/:id', jsonParser, (request, response) => {
	let column = request.body.editProp;
	let value = request.body.editVal;

});

// Delete a medication from the medication table
app.delete('/medication/:id', jsonParser, (request, response) => {

});

/*---------- RUN SERVER FUNCTION ----------*/

function runServer(callback) {
    let PORT = process.env.PORT || 8080;
    app.listen(PORT, () => {
        console.log(`Listening on localhost: ${PORT}`);
        if (callback) {
            callback();
        }
    });
}

if (require.main === module) {
    runServer((err) => {
        if (err) {
            throw new Error(err);
        }
    });
}<commit_msg>adds get medications endpoint to server.js<commit_after>/*---------- DEPENDENCIES ----------*/

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const jsonParser = bodyParser.json();
const knex = require('./backend/database/connect')

/*--------- EXPRESS CONFIG ----------*/

// Serve the static frontend files
app.use(express.static('./build'));

// Use JSON Parser everywhere
app.use(jsonParser);

// CORs handling
app.use((request, response, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  next();
});

/*---------- DATABASE QUERIES ----------*/

// Select all medication entries from medication table
app.get('/medication', jsonParser, (request, response) => {
	let time = new Date().getTime() / 1000;
	let dueMeds = [];
	let upcomingMeds = [];
	knex.select()
        .from('medication')
        .orderBy('next_dose_secs')
        .then((data) => {
        	data.forEach((med) => {
        		if (time > next_dose_secs) {
        			dueMeds.push(med);
        		} else {
        			upcomingMeds.push(med);
        		}
        	});
        	return response.status(200).json({dueMeds: dueMeds, upcomingMeds: upcomingMeds});
        }).catch((err) => {
            response.status(500).json({'error: ' error});
        });
)};

// Select the details of a specific medication by id
app.get('/medication/:id', jsonParser, (request, response) => {
	knex.select()
        .from('questions')
        .where({ is_answered: false })
        .orderBy('when_asked', 'desc')
        .then((questions) => {
            // same nested loop to add tags to question objects before resolving
            resolve({ questions: questions });
        }).catch((err) => {
            reject(err);
        });

});

// Select all past doses from dose_history table
app.get('/medication/history', jsonParser, (request, response) => {

})

// Add a new medication to the medication table
app.post('/medication/new', jsonParser, (request, response) => {
	let name = request.body.name;
	let dosage = request.body.dosage;
	let numDoses = request.body.numDoses;
	let firstDose = request.body.firstDose;
	let instructions = request.body.instructions;
	let precautions = request.body.precautions;

	let frequency = Math.floor(24 / numDoses);
	frequecy = 3600 * frequency;
	firstDose = 3600 * firstDose;
	let time = new Date().getTime() / 1000;
	let nextDoseSecs = firstDose + time;
	let nextDoseDate = new Date(0);
	nextDoseDate.setUTCSeconds(nextDose);

	knex.insert({ name: name, dosage: dosage, num_doses: numDoses, frequency: frequency, next_dose_secs: nextDoseSecs, next_dose_date: nextDoseDate, instructions: instrucstions, precautions: precautions })
        .returning(['name', 'dosage', 'num_doses', 'next_dose_secs', 'next_dose_date', 'instructions', 'precautions'])
        .into('medication')
        .then((data) => {
            return response.status(201).json({
            	name: data[0].name, dosage: data[0].dosage, numDoses: data[0].num_doses, nextDoseSecs: data[0].next_dose_secs, nextDoseDate: data[0].next_dose_date, instructions: data[0].instrucstion, precautions: data[0].precautions
            });
        })
        .catch((error) => {
            response.status(500).json({'error: ' error});
        });
});

// Edit the details of an existing medication
app.put('/medication/update/:id', jsonParser, (request, response) => {
	let column = request.body.editProp;
	let value = request.body.editVal;

});

// Delete a medication from the medication table
app.delete('/medication/:id', jsonParser, (request, response) => {

});

/*---------- RUN SERVER FUNCTION ----------*/

function runServer(callback) {
    let PORT = process.env.PORT || 8080;
    app.listen(PORT, () => {
        console.log(`Listening on localhost: ${PORT}`);
        if (callback) {
            callback();
        }
    });
}

if (require.main === module) {
    runServer((err) => {
        if (err) {
            throw new Error(err);
        }
    });
}<|endoftext|>


<commit_before>var superagent = require('superagent'),
	RLS = require('./RequestLocalStorage').getNamespace(),
	logger = require('../logging').getLogger(__LOGGER__),
	config = require("../config"),
	Q = require('q');

/**
 * Wrapper around superagent.Request. Passes through function calls
 * to the wrapped request, except for end() which it enhances with
 * caching for server-side requests.
 */
function Request(req) {
	logger.debug("Making wrapped request");
	this._req = req;
}

// mix in properties from superagent.Request,
// but skip the 'end' method -- we'll do that manaully
Object.keys(superagent.Request.prototype)
	.filter( propName => propName !== 'end' )
	.forEach( propName => {
		var originalProp = superagent.Request.prototype[propName];
		if (typeof originalProp === 'function') {
			Request.prototype[propName] = () => originalProp.apply(this._req, arguments);
		} else {
			Request.prototype[propName] = originalProp;
		}
	});

/**
 * Wrap superagent's end() method to create an entry in a request-local
 * data cache that will be serialized to the client and replayed in the
 * browser.
 */
Request.prototype.end = function (fn) {
	var superagentRequestEnd = superagent.Request.prototype.end;

	// TODO: support both (err, res) and (res) function signatures
	if (fn.length !== 2) {
		throw `Callback passed to end() must be the two-arg version: (err, res)`;
	}

	// only do caching for responses for GET requests for now
	if (this._req.method !== 'GET') {
		return superagentRequestEnd.apply(this._req, arguments);
	}

	var url = this._req.url;

	// get cache entry for url if exists; if server-side, create one if it doesn't already exist
	var entry = makeRequest.cache().entry(url, SERVER_SIDE /* createIfMissing */);
	if (!SERVER_SIDE && !entry) {
		return superagentRequestEnd.apply(this._req, arguments);
	}

	// no previous requesters? fire the request
	if (entry.requesters === 0) {
		superagentRequestEnd.call(this._req, function (err, res) {
			if (err) {
				entry.setError(err);
				return;
			}
			entry.setResponse(res);
		});
	}

	entry.whenDataReady().then( (res) => {
		fn(null, res);
	}, (err) => {
		fn(err);
	});

	return this;
}

// wrapper for superagent
function makeRequest (method, url) {
	var req = superagent.apply(null, arguments);

	// add default headers to request (if necessary)
	var defaultHeaders = makeRequest.defaultHeaders();
	if (defaultHeaders) {
		req.set(defaultHeaders);
	}

	return new Request(req);
}
module.exports = makeRequest;

makeRequest.get = function (url, data, fn) {
	var req = makeRequest('GET', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.query(data);
	if (fn) req.end(fn);
	return req;
}

makeRequest.head = function (url, data, fn){
	var req = makeRequest('HEAD', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.del = function (url, fn){
	var req = makeRequest('DELETE', url);
	if (fn) req.end(fn);
	return req;
};

makeRequest.patch = function (url, data, fn){
	var req = makeRequest('PATCH', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.post = function (url, data, fn){
	var req = makeRequest('POST', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.put = function (url, data, fn){
	var req = makeRequest('PUT', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.cache = function () {
	if (!SERVER_SIDE) {
		var cache = window.__cache;
		if (!cache) {
			cache = window.__cache = new RequestDataCache();
		}
		return cache;
	}

	// TODO: why doesn't this work in the browser!?
	var cache = RLS().cache;
	if (!cache) {
		cache = RLS().cache = new RequestDataCache();
	}
	return cache;
}

/**
 * Get or set the default headers for TritonAgent's http requests.
 * If `headers` is specified, set them; else, return them.
 */
makeRequest.defaultHeaders = function (headers) {
	if (SERVER_SIDE) {
		if (typeof headers === 'undefined') {
			return RLS().defaultHeaders;
		}
		RLS().defaultHeaders = headers;
	} else {
		logger.warning("Attempted to set defaultHeaders in TritonAgent on the client");
	}
}

/**
 * An entry in the RequestDataCache
 */
class CacheEntry {

	constructor (cache, url) {
		this.cache = cache;
		this.url = url;
		this.requesters = 0;
		this.dfd = Q.defer();
		this.dfd.promise.fin/*ally*/( () => logger.debug(`Data resolved for: ${url}`) );
		this.loaded = false;
		this.res = undefined;
		this.err = undefined;
	}

	dehydrate () {
		return {
			url: this.url,
			requesters: this.requesters,
			loaded: this.loaded,
			res: this.res,
			err: this.err // TODO: does this do something reasonable for Error objects?
		};
	}

	rehydrate (state) {
		this.url = state.url;
		this.requesters = state.requesters;
		this.loaded = state.loaded;
		this.res = state.res;
		this.err = state.err;

		// TODO FIXME: these won't work if the response from the server was an error

		if (this.loaded) {
			// call setResponse to resolve the deferred
			if (this.res) {
				this.setResponse(this.res);
			} else if (this.err) {
				this.setError(this.err);
			}
			logger.debug(`Rehydrating resolved url to cache: ${this.url}`);
		} else {
			logger.debug(`Rehydrating pending url to cache without data: ${this.url}`);
		}
	}

	setResponse (res) {
		// TODO: store superagent response? or body? or payload?
		this.res = this._copyResponseData(res);
		this.loaded = true;
		this.dfd.resolve(this.res);
	}

	setError (err) {
		this.err = err;
		this.loaded = true;
		this.dfd.reject(this.err);
	}

	whenDataReady () {
		if (SERVER_SIDE) {
			// server-side, we increment the number of requesters
			// we expect to retrieve the data on the frontend
			this.requesters += 1;
			return this.dfd.promise;
		} else {
			// client-side, whenever someone retrieves data from the cache,
			// we decrement the number of retrievals expected, and when we
			// hit zero, remove the cache entry.
			return this._requesterDecrementingPromise(this.dfd.promise);
		}
	}

	// for internal (triton middleware) calls
	whenDataReadyInternal () {
		return this.dfd.promise;
	}

	decrementRequesters () {
		logger.debug("Decrementing: " + this.url);
		this.requesters -= 1;

		if (this.requesters === 0) {
			delete this.cache.dataCache[this.url];
		}

		this.cache.checkCacheDepleted();
	}

	/**
	 * Chain a promise with another promise that decrements
	 * the number of expected requesters.
	 */
	_requesterDecrementingPromise (promise) {
		// regardless of whether we're resolved with a 'res' or 'err',
		// we want to decrement requests. the appropriate 'success' or 'error'
		// callback will be executed on whatever is chained after this method
		return promise.fin( resOrErr => {
			this.decrementRequesters();
			return resOrErr;
		});
	}

	// create a copy of the data on the superagent response suitable
	// for writing as JSON
	_copyResponseData (res) {
		var result = {};
		[
			"body",
			"text",
			/*'files'*/ // TODO
			"header",
			"status",
			"statusType",
			"info",
			"ok",
			"clientError",
			"serverError",
			"error",

			"accepted",
			"noContent",
			"badRequest",
			"unauthorized",
			"notAcceptable",
			"notFound",
			"forbidden"
		].forEach( prop => {
			result[prop] = res[prop];
		});

		return result;
	}

}

/**
 * Cache of responses to API requests made server-side that will be
 * serialized as part of the initial page request and replayed in the
 * browser.
 */
class RequestDataCache {

	constructor () {
		this.dataCache = {};
	}

	dehydrate () {

		var out = {};

		out.dataCache = {};

		var dataCache = this.dataCache;
		Object.keys(dataCache).forEach(function (url) {
			var result = dataCache[url];
			out.dataCache[url] = result.dehydrate();
		});

		return out;
	}

	rehydrate (state) {

		logger.debug("Rehydrating RequestDataCache");

		// clear state
		var dataCache = this.dataCache = {};

		Object.keys(state.dataCache).forEach( (url) => {
			var cacheEntry = dataCache[url] = new CacheEntry(this, url);
			cacheEntry.rehydrate(state.dataCache[url]);
		});

	}

	/**
	 * Get (optionally creating if necessary) the entry for the given
	 * URL from the cache.
	 *
	 * @param createIfMissing boolean default true
	 */
	entry (url, createIfMissing) {
		if (typeof createIfMissing === 'undefined') {
			createIfMissing = true;
		}
		logger.debug(`Getting cache entry for ${url}`)

		var cacheEntry = this.dataCache[url];
		if (!cacheEntry && createIfMissing) {
			cacheEntry = this.dataCache[url] = new CacheEntry(this, url);
		}

		return cacheEntry;
	}

	/**
	 * Synchronously check if data is loaded already.
	 * Returns an object with a getData() function, to
	 * make it possible to check for the existence of a URL
	 * in the cache, but not actually retrieve if (if desired).
	 * Calling getData() will retrieve the data from the cache
	 * and decrement the number of requesters
	 */
	checkLoaded (url) {
		var cached = this.dataCache[url];

		if (cached && cached.res) {
			return {
				getData: () => {
					// sort of a synchronous promise thing
					cached.decrementRequesters();
					return cached.res;
				}
			};
		}
		return null;
	}

	getPendingRequests () {
		return this.getAllRequests().filter(req => !req.entry.loaded);
	}

	getAllRequests() {
		return Object.keys(this.dataCache)
			.map(
				url => (
					{
						url   : url,
						entry : this.dataCache[url],
					}
				)
			);
	}

	whenAllPendingResolve () {
		var promises = this.getAllRequests().map(req => req.entry.promise);
		return Q.allSettled(promises);
	}

	/**
	 * Fires when the cache has been completely depleted, which is used as a signal to render when there was a timeout on the server.
	 */
	whenCacheDepleted () {
		this.whenCacheDepletedDfd = this.whenCacheDepletedDfd || Q.defer();

		this.checkCacheDepleted();

		return this.whenCacheDepletedDfd.promise;
	}

	checkCacheDepleted() {
		logger.debug("_checkCacheDepleted");
		if (this.whenCacheDepletedDfd) {
			var totalRequestersPending = 0;
			Object.keys(this.dataCache).forEach((name) => {
				if (this.dataCache[name].loaded) {
					totalRequestersPending += this.dataCache[name].requesters;
				}
			});
			logger.debug(`Checking for depleted cache, with ${totalRequestersPending} requesters left`);
			if (totalRequestersPending === 0) this.whenCacheDepletedDfd.resolve();
		}
	}

	lateArrival (url, res) {
		logger.debug(`Late arrival for ${url}`);
		var dataCache = this.dataCache;
		if (dataCache[url]) {
			dataCache[url].setResponse(res);
		} else {
			logger.debug("WTF?");
		}
	}

}
<commit_msg>RequestDataCache.entry() defaults to not creating an entry<commit_after>var superagent = require('superagent'),
	RLS = require('./RequestLocalStorage').getNamespace(),
	logger = require('../logging').getLogger(__LOGGER__),
	config = require("../config"),
	Q = require('q');

/**
 * Wrapper around superagent.Request. Passes through function calls
 * to the wrapped request, except for end() which it enhances with
 * caching for server-side requests.
 */
function Request(req) {
	logger.debug("Making wrapped request");
	this._req = req;
}

// mix in properties from superagent.Request,
// but skip the 'end' method -- we'll do that manaully
Object.keys(superagent.Request.prototype)
	.filter( propName => propName !== 'end' )
	.forEach( propName => {
		var originalProp = superagent.Request.prototype[propName];
		if (typeof originalProp === 'function') {
			Request.prototype[propName] = () => originalProp.apply(this._req, arguments);
		} else {
			Request.prototype[propName] = originalProp;
		}
	});

/**
 * Wrap superagent's end() method to create an entry in a request-local
 * data cache that will be serialized to the client and replayed in the
 * browser.
 */
Request.prototype.end = function (fn) {
	var superagentRequestEnd = superagent.Request.prototype.end;

	// TODO: support both (err, res) and (res) function signatures
	if (fn.length !== 2) {
		throw `Callback passed to end() must be the two-arg version: (err, res)`;
	}

	// only do caching for responses for GET requests for now
	if (this._req.method !== 'GET') {
		return superagentRequestEnd.apply(this._req, arguments);
	}

	var url = this._req.url;

	// get cache entry for url if exists; if server-side, create one if it doesn't already exist
	var entry = makeRequest.cache().entry(url, SERVER_SIDE /* createIfMissing */);
	if (!SERVER_SIDE && !entry) {
		return superagentRequestEnd.apply(this._req, arguments);
	}

	// no previous requesters? fire the request
	if (entry.requesters === 0) {
		superagentRequestEnd.call(this._req, function (err, res) {
			if (err) {
				entry.setError(err);
				return;
			}
			entry.setResponse(res);
		});
	}

	entry.whenDataReady().then( (res) => {
		fn(null, res);
	}, (err) => {
		fn(err);
	});

	return this;
}

// wrapper for superagent
function makeRequest (method, url) {
	var req = superagent.apply(null, arguments);

	// add default headers to request (if necessary)
	var defaultHeaders = makeRequest.defaultHeaders();
	if (defaultHeaders) {
		req.set(defaultHeaders);
	}

	return new Request(req);
}
module.exports = makeRequest;

makeRequest.get = function (url, data, fn) {
	var req = makeRequest('GET', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.query(data);
	if (fn) req.end(fn);
	return req;
}

makeRequest.head = function (url, data, fn){
	var req = makeRequest('HEAD', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.del = function (url, fn){
	var req = makeRequest('DELETE', url);
	if (fn) req.end(fn);
	return req;
};

makeRequest.patch = function (url, data, fn){
	var req = makeRequest('PATCH', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.post = function (url, data, fn){
	var req = makeRequest('POST', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.put = function (url, data, fn){
	var req = makeRequest('PUT', url);
	if ('function' == typeof data) fn = data, data = null;
	if (data) req.send(data);
	if (fn) req.end(fn);
	return req;
};

makeRequest.cache = function () {
	if (!SERVER_SIDE) {
		var cache = window.__cache;
		if (!cache) {
			cache = window.__cache = new RequestDataCache();
		}
		return cache;
	}

	// TODO: why doesn't this work in the browser!?
	var cache = RLS().cache;
	if (!cache) {
		cache = RLS().cache = new RequestDataCache();
	}
	return cache;
}

/**
 * Get or set the default headers for TritonAgent's http requests.
 * If `headers` is specified, set them; else, return them.
 */
makeRequest.defaultHeaders = function (headers) {
	if (SERVER_SIDE) {
		if (typeof headers === 'undefined') {
			return RLS().defaultHeaders;
		}
		RLS().defaultHeaders = headers;
	} else {
		logger.warning("Attempted to set defaultHeaders in TritonAgent on the client");
	}
}

/**
 * An entry in the RequestDataCache
 */
class CacheEntry {

	constructor (cache, url) {
		this.cache = cache;
		this.url = url;
		this.requesters = 0;
		this.dfd = Q.defer();
		this.dfd.promise.fin/*ally*/( () => logger.debug(`Data resolved for: ${url}`) );
		this.loaded = false;
		this.res = undefined;
		this.err = undefined;
	}

	dehydrate () {
		return {
			url: this.url,
			requesters: this.requesters,
			loaded: this.loaded,
			res: this.res,
			err: this.err // TODO: does this do something reasonable for Error objects?
		};
	}

	rehydrate (state) {
		this.url = state.url;
		this.requesters = state.requesters;
		this.loaded = state.loaded;
		this.res = state.res;
		this.err = state.err;

		// TODO FIXME: these won't work if the response from the server was an error

		if (this.loaded) {
			// call setResponse to resolve the deferred
			if (this.res) {
				this.setResponse(this.res);
			} else if (this.err) {
				this.setError(this.err);
			}
			logger.debug(`Rehydrating resolved url to cache: ${this.url}`);
		} else {
			logger.debug(`Rehydrating pending url to cache without data: ${this.url}`);
		}
	}

	setResponse (res) {
		// TODO: store superagent response? or body? or payload?
		this.res = this._copyResponseData(res);
		this.loaded = true;
		this.dfd.resolve(this.res);
	}

	setError (err) {
		this.err = err;
		this.loaded = true;
		this.dfd.reject(this.err);
	}

	whenDataReady () {
		if (SERVER_SIDE) {
			// server-side, we increment the number of requesters
			// we expect to retrieve the data on the frontend
			this.requesters += 1;
			return this.dfd.promise;
		} else {
			// client-side, whenever someone retrieves data from the cache,
			// we decrement the number of retrievals expected, and when we
			// hit zero, remove the cache entry.
			return this._requesterDecrementingPromise(this.dfd.promise);
		}
	}

	// for internal (triton middleware) calls
	whenDataReadyInternal () {
		return this.dfd.promise;
	}

	decrementRequesters () {
		logger.debug("Decrementing: " + this.url);
		this.requesters -= 1;

		if (this.requesters === 0) {
			delete this.cache.dataCache[this.url];
		}

		this.cache.checkCacheDepleted();
	}

	/**
	 * Chain a promise with another promise that decrements
	 * the number of expected requesters.
	 */
	_requesterDecrementingPromise (promise) {
		// regardless of whether we're resolved with a 'res' or 'err',
		// we want to decrement requests. the appropriate 'success' or 'error'
		// callback will be executed on whatever is chained after this method
		return promise.fin( resOrErr => {
			this.decrementRequesters();
			return resOrErr;
		});
	}

	// create a copy of the data on the superagent response suitable
	// for writing as JSON
	_copyResponseData (res) {
		var result = {};
		[
			"body",
			"text",
			/*'files'*/ // TODO
			"header",
			"status",
			"statusType",
			"info",
			"ok",
			"clientError",
			"serverError",
			"error",

			"accepted",
			"noContent",
			"badRequest",
			"unauthorized",
			"notAcceptable",
			"notFound",
			"forbidden"
		].forEach( prop => {
			result[prop] = res[prop];
		});

		return result;
	}

}

/**
 * Cache of responses to API requests made server-side that will be
 * serialized as part of the initial page request and replayed in the
 * browser.
 */
class RequestDataCache {

	constructor () {
		this.dataCache = {};
	}

	dehydrate () {

		var out = {};

		out.dataCache = {};

		var dataCache = this.dataCache;
		Object.keys(dataCache).forEach(function (url) {
			var result = dataCache[url];
			out.dataCache[url] = result.dehydrate();
		});

		return out;
	}

	rehydrate (state) {

		logger.debug("Rehydrating RequestDataCache");

		// clear state
		var dataCache = this.dataCache = {};

		Object.keys(state.dataCache).forEach( (url) => {
			var cacheEntry = dataCache[url] = new CacheEntry(this, url);
			cacheEntry.rehydrate(state.dataCache[url]);
		});

	}

	/**
	 * Get (optionally creating if necessary) the entry for the given
	 * URL from the cache.
	 *
	 * @param createIfMissing boolean default false
	 */
	entry (url, createIfMissing) {
		if (typeof createIfMissing === 'undefined') {
			createIfMissing = false;
		}
		logger.debug(`Getting cache entry for ${url}`)

		var cacheEntry = this.dataCache[url];
		if (!cacheEntry && createIfMissing) {
			cacheEntry = this.dataCache[url] = new CacheEntry(this, url);
		}

		return cacheEntry;
	}

	/**
	 * Synchronously check if data is loaded already.
	 * Returns an object with a getData() function, to
	 * make it possible to check for the existence of a URL
	 * in the cache, but not actually retrieve if (if desired).
	 * Calling getData() will retrieve the data from the cache
	 * and decrement the number of requesters
	 */
	checkLoaded (url) {
		var cached = this.dataCache[url];

		if (cached && cached.res) {
			return {
				getData: () => {
					// sort of a synchronous promise thing
					cached.decrementRequesters();
					return cached.res;
				}
			};
		}
		return null;
	}

	getPendingRequests () {
		return this.getAllRequests().filter(req => !req.entry.loaded);
	}

	getAllRequests() {
		return Object.keys(this.dataCache)
			.map(
				url => (
					{
						url   : url,
						entry : this.dataCache[url],
					}
				)
			);
	}

	whenAllPendingResolve () {
		var promises = this.getAllRequests().map(req => req.entry.promise);
		return Q.allSettled(promises);
	}

	/**
	 * Fires when the cache has been completely depleted, which is used as a signal to render when there was a timeout on the server.
	 */
	whenCacheDepleted () {
		this.whenCacheDepletedDfd = this.whenCacheDepletedDfd || Q.defer();

		this.checkCacheDepleted();

		return this.whenCacheDepletedDfd.promise;
	}

	checkCacheDepleted() {
		logger.debug("_checkCacheDepleted");
		if (this.whenCacheDepletedDfd) {
			var totalRequestersPending = 0;
			Object.keys(this.dataCache).forEach((name) => {
				if (this.dataCache[name].loaded) {
					totalRequestersPending += this.dataCache[name].requesters;
				}
			});
			logger.debug(`Checking for depleted cache, with ${totalRequestersPending} requesters left`);
			if (totalRequestersPending === 0) this.whenCacheDepletedDfd.resolve();
		}
	}

	lateArrival (url, res) {
		logger.debug(`Late arrival for ${url}`);
		var dataCache = this.dataCache;
		if (dataCache[url]) {
			dataCache[url].setResponse(res);
		} else {
			logger.debug("WTF?");
		}
	}

}
<|endoftext|>


<commit_before>/**
 * Created by kimown on 16-9-14.
 */

'use strict';

let {request,rmFileSync}= require('./../util');
let logger = require('./../logger');
let loggerfile = require('./../loggerfile');
let {writeUserData,readUserData}=require('./common');
let configPath=require('./common/config-path');

process.on('uncaughtException', (err) => {
    console.log(err);
});

process.on('exit',(code) => {
    //如果程序异常退出且 totalWordCount 没有达到 一共要猜测的单词数量 ,保存用户数据
    // saveUserDataSync();
    console.log(`程序退出了About to exit with code: ${code}`);
});

let CONFIG={};

async function main() {

    //里面保存了所有的用户数据．
    CONFIG= await readUserData();

    if(CONFIG.hasOwnProperty('sessionId')&&CONFIG.sessionId){
        //断线重玩游戏
        await makeAGuess();
    }else{
        //new 一个game
        await startGame();
    }
}

/**
 * 1. Start Game
 * https://github.com/strikingly/strikingly-interview-test-instructions#1-start-game
 */
async function startGame() {
    let postData = {
        playerId: CONFIG.playerId,
        action: CONFIG.action.startGame
    };
    let res = await request({
        method: 'post',
        body: postData,
        json: true,
        url: CONFIG.requestUrl
    });
    let {body, ok}=res;
    if (ok == true) {
        let {data, message, sessionId}=body;
        let {numberOfGuessAllowedForEachWord, numberOfWordsToGuess}=data;
        logger.info(`message: ${message} ,您的身份标示是 ${sessionId} ,您一共要猜测 ${numberOfWordsToGuess} 个单词,每个单词限制猜测 ${numberOfGuessAllowedForEachWord} 次`);

        CONFIG.sessionId = sessionId;
        CONFIG.numberOfGuessAllowedForEachWord = numberOfGuessAllowedForEachWord;
        CONFIG.numberOfWordsToGuess = numberOfWordsToGuess;

    } else {
        logger.error(`启动游戏失败!!!失败原因:`, res.body);
    }

    await GiveMeAWord();

}

/**
 *　2. Give Me A Word
 * https://github.com/strikingly/strikingly-interview-test-instructions#2-give-me-a-word
 */
async function GiveMeAWord() {

    await checkArriveLimitWords();

    let postData = {
        sessionId: CONFIG.sessionId,
        action: CONFIG.action.nextWord
    };
    let res = await request({
        method: 'post',
        body: postData,
        json: true,
        url: CONFIG.requestUrl
    });
    let {body, ok}=res;
    if (ok == true) {
        let {data}=body;
        let {totalWordCount, word, wrongGuessCountOfCurrentWord}=data;
        CONFIG.currentGuessWord.totalWordCount = totalWordCount;
        CONFIG.currentGuessWord.word = word;
        CONFIG.currentGuessWord.wrongGuessCountOfCurrentWord = wrongGuessCountOfCurrentWord;
        await makeAGuess(CONFIG);
    } else {
        logger.error(`获取猜谜单词失败!!!失败原因:`, res.body);
    }
}

/**
 * 3.Make A Guess
 * https://github.com/strikingly/strikingly-interview-test-instructions#3-make-a-guess
 * 猜测当前的单词
 */
async function makeAGuess() {

    let isNeedNextWord=  checkNeedNextWord();

    if(isNeedNextWord){
        initCurrentGuessWordStatus();
        await GiveMeAWord();
    }else {
        let {currentGuessWord, numberOfGuessAllowedForEachWord}=CONFIG;
        let {wrongGuessCountOfCurrentWord, totalWordCount, word,currentGuessCount}=currentGuessWord;
        logger.info(`第${totalWordCount}个单词: 第${currentGuessCount}次猜测,单词内容是 ${word},已经猜错了${wrongGuessCountOfCurrentWord}次(${numberOfGuessAllowedForEachWord})`);

        let mostPossibleChar = getBestMatchChar();

        let postData = {
            sessionId: CONFIG.sessionId,
            action: CONFIG.action.guessWord,
            guess: mostPossibleChar
        };
        let res = await request({
            method: 'post',
            body: postData,
            json: true,
            url: CONFIG.requestUrl
        });

        //TODO 如果mostPossibleChar不在返回单词里面，去除含有mostPossibleChar的单词
        setCurrentGuessStatus(res);

        //如果字符没有猜对
        if(!checkGuessCorrect(mostPossibleChar)){
            CONFIG.currentGuessWord.alreadyConfirmWrongCharAr.push(mostPossibleChar);
        }

        //如果单词没有全部猜对，递归调用猜词方法;否则直接nextWord
        //如果正在猜测的错误次数超过允许猜测的最大次数,nextWorld
        if(checkNeedNextWord()||checkWordCotainAsterisks()){
            await makeAGuess();
        }else{
            loggerfile.info(`-----　第${totalWordCount}个单词猜测成功，答案是 ${CONFIG.currentGuessWord.word} ------`);
            await wordHasGuessedOperation();
        }
    }
}

/**
 * 检查发送的字符是否正确
 */
function checkGuessCorrect(mostPossibleChar) {
    let {currentGuessWord}=CONFIG;
    let { word}=currentGuessWord;
    return word.includes(mostPossibleChar)
}

/**
 * 是否需要下一个单词 ,默认走正常猜词流程
 */
function checkNeedNextWord() {
    let flag=false;
    let {currentGuessWord, numberOfGuessAllowedForEachWord}=CONFIG;
    let {wrongGuessCountOfCurrentWord, totalWordCount,currentGuessCount}=currentGuessWord;

    //如果正在猜测的错误次数超过允许猜测的最大次数 或者　超过20次
    if (wrongGuessCountOfCurrentWord == numberOfGuessAllowedForEachWord) {
        loggerfile.info(`第${totalWordCount}个单词: 第${currentGuessCount}次猜测, 猜错次数已经达到默认上限 ${numberOfGuessAllowedForEachWord} ，开始下一轮.......failing`);
        flag=true;
    }else if(currentGuessCount>20){
        loggerfile.info(`第${totalWordCount}个单词: 第${currentGuessCount}次猜测, 猜测次数已经超过 20　 ，开始下一轮.......failing`);
        flag=true;
    }
    return flag;
}

async function checkArriveLimitWords() {
    let {currentGuessWord,numberOfWordsToGuess}=CONFIG;
    let {totalWordCount}=currentGuessWord;
    //如果正在猜的单词数等于一共要猜测的单词数量，GAME OVER,查询分数
    if(totalWordCount>numberOfWordsToGuess){
        await GetYourResult();
        processExit();
        return;
    }
}

/**
 * 如果猜中了单词，一些后续操作
 * -　从整体的单词库里面删除这个单词
 *
 */
async function wordHasGuessedOperation() {
    let {allWordsArray,wordsHasGuessed,currentGuessWord}=CONFIG;
    let {word}=currentGuessWord;
    CONFIG.wordsHasGuessed.push(word);

    //allWordsArray 里面删除这个单词
    let positiion=allWordsArray.indexOf(word);
    if(positiion!=-1){
        CONFIG.allWordsArray=CONFIG.allWordsArray.splice(positiion,1);
        loggerfile.info(`-----　删除 ${word} 后剩余 ${CONFIG.allWordsArray.length}　个单词　------`);
    }
    initCurrentGuessWordStatus();
    await GiveMeAWord();
}

/**
 *
 * 4. Get Your Result
 */
async function GetYourResult(){
    let postData = {
        sessionId: CONFIG.sessionId,
        action: CONFIG.action.getResult
    };
    let res = await request({
        method: 'post',
        body: postData,
        json: true,
        url: CONFIG.requestUrl
    });
    loggerfile.info(`最后得分`,res);
}

function processExit() {
    loggerfile.info(`主动退出程序，游戏结束,之后pm2会自动重启,然后开启新一轮的游戏`);
    //删除data.json
    let {userDataPath}=configPath;
    rmFileSync(userDataPath);
}

/**
 * 初始化当前猜词状态
 */
function initCurrentGuessWordStatus() {
    let {currentGuessWord}=CONFIG;
    currentGuessWord.word=null;
    currentGuessWord.alreadyConfirmWrongCharAr=[];
    currentGuessWord.currentGuessCount=1;
    currentGuessWord.alreadyRequestCharAr=[];
    currentGuessWord.alreadyConfirmWrongCharAr=[];
    //重点是这个猜词的个数+1
    currentGuessWord.totalWordCount=++currentGuessWord.totalWordCount;
    currentGuessWord.wrongGuessCountOfCurrentWord=0;
}

/**
 * 判断单词是否含有*号
 */
function checkWordCotainAsterisks() {
    let {currentGuessWord}=CONFIG;
    let { word}=currentGuessWord;
    return word.includes('*');
}

/**
 * 判断单词中是否全部都是*号
 */
function checkWordAllAsterisks() {
    let {currentGuessWord}=CONFIG;
    let { word}=currentGuessWord;
    return word.split('').every((v)=>{
        return v=='*'
    })
}

/**
 * 针对不同的响应，更新当前的猜词状态
 * @param res
 */
function setCurrentGuessStatus(res) {
    let {body, ok}=res;
    let {data}=body;

    let {totalWordCount, word, wrongGuessCountOfCurrentWord}=data;
    if (ok == true) {
        CONFIG.currentGuessWord.totalWordCount = totalWordCount;
        CONFIG.currentGuessWord.word = word;
        CONFIG.currentGuessWord.wrongGuessCountOfCurrentWord = wrongGuessCountOfCurrentWord;
        CONFIG.currentGuessWord.currentGuessCount = ++CONFIG.currentGuessWord.currentGuessCount;
    }
}

/**
 * 思路：先获取单词的length ,只过滤剩下掉指定长度的word,然后这些word里面循环判断26个字母哪个字母的出现频率最高（同一个字母中重复字符忽略不计数）
 *
 * 获取最可能的字符
 */
function getBestMatchChar() {
    let {currentGuessWord, allWordsArray}=CONFIG;
    let {word, totalWordCount,alreadyRequestCharAr,alreadyConfirmWrongCharAr}=currentGuessWord;
    let wordLength = word.length;
    let allWordsArrayAfterSpecifyLenth = allWordsArray.filter((v)=> {
        return v.trim().length == wordLength && v.trim().indexOf("'") == -1;
    })
    logger.info(`第${totalWordCount}个单词: 先按照长度为 ${wordLength}　的英文单词过滤，过滤后共剩下 ${allWordsArrayAfterSpecifyLenth.length}个`);

    if(!checkWordAllAsterisks()){
        allWordsArrayAfterSpecifyLenth=allWordsArrayAfterSpecifyLenth.filter((v)=>{
            v=v.toUpperCase();
            let flag=true;
            word.split('').map((singleChar,k)=>{
                if(singleChar!='*'){
                    if(v[k]!=singleChar){
                        flag=false;
                    }
                }
            })
            return flag;
        })
        logger.info(`第${totalWordCount}个单词:　由于不是全*号，按照单词内容 ${word}　过滤, 过滤后共剩下 ${allWordsArrayAfterSpecifyLenth.length}个`);
    }

    if(alreadyConfirmWrongCharAr.length>0){
        allWordsArrayAfterSpecifyLenth=allWordsArrayAfterSpecifyLenth.filter((v)=>{
            let flag=true;
            if(checkStrContainEveryCharAr(v,alreadyConfirmWrongCharAr)){
                flag=false;
            }
            return flag;
        });
        logger.info(`第${totalWordCount}个单词:　排除掉含有 ${alreadyConfirmWrongCharAr.toString()} 的单词, 过滤后共剩下 ${allWordsArrayAfterSpecifyLenth.length}个`);
    }

    let objCount = {};
    allWordsArrayAfterSpecifyLenth.map((item)=> {
        let s = new Set();
        for (let i in item) {
            //如果是已经发送的猜测字符，那么下一次不继续发送了
            if(!alreadyRequestCharAr.includes(item[i].toUpperCase())){
                s.add(item[i]);
            }
        }
        for (let key of s.values()) {
            if (objCount[key]) {
                objCount[key] = (++objCount[key]);
            } else {
                objCount[key] = 1;
            }
        }
    })
    let maxKey;
    for (let i in objCount) {
        if (!maxKey) {
            maxKey = i;
        } else if (maxKey && objCount[maxKey] < objCount[i]) {
            maxKey = i;
        }
    }
    //TODO 如果没有匹配的字符，这里应该使用一个随机字符
    if(!maxKey){
        return 'E';
    }
    let mostPossibleChar = maxKey.toUpperCase();
    logger.info(`第${totalWordCount}个单词: 按照出现频率最高的字符是 ${mostPossibleChar} `);

    CONFIG.currentGuessWord.alreadyRequestCharAr.push(mostPossibleChar);

    return mostPossibleChar;
}

/**
 * 监测单词里面是否含有指定的字符数组.
 * 默认单词里面包含所有的字符数组
 */
function checkStrContainEveryCharAr(str,charAr) {
    let flag=true;
    charAr.map((char)=>{
        if(!str.includes(char)){
            flag=false;
        }
    })
    return flag;
}

function saveUserDataSync() {
    writeUserData(JSON.stringify(CONFIG,null,2));
}

main();
<commit_msg>splice返回的是删除的数组元素<commit_after>/**
 * Created by kimown on 16-9-14.
 */

'use strict';

let {request,rmFileSync}= require('./../util');
let logger = require('./../logger');
let loggerfile = require('./../loggerfile');
let {writeUserData,readUserData}=require('./common');
let configPath=require('./common/config-path');

process.on('uncaughtException', (err) => {
    console.log(err);
});

process.on('exit',(code) => {
    //如果程序异常退出且 totalWordCount 没有达到 一共要猜测的单词数量 ,保存用户数据
    saveUserDataSync();
    console.log(`程序退出了About to exit with code: ${code}`);
});

let CONFIG={};

async function main() {

    //里面保存了所有的用户数据．
    CONFIG= await readUserData();

    if(CONFIG.hasOwnProperty('sessionId')&&CONFIG.sessionId){
        //断线重玩游戏
        await makeAGuess();
    }else{
        //new 一个game
        await startGame();
    }
}

/**
 * 1. Start Game
 * https://github.com/strikingly/strikingly-interview-test-instructions#1-start-game
 */
async function startGame() {
    let postData = {
        playerId: CONFIG.playerId,
        action: CONFIG.action.startGame
    };
    let res = await request({
        method: 'post',
        body: postData,
        json: true,
        url: CONFIG.requestUrl
    });
    let {body, ok}=res;
    if (ok == true) {
        let {data, message, sessionId}=body;
        let {numberOfGuessAllowedForEachWord, numberOfWordsToGuess}=data;
        logger.info(`message: ${message} ,您的身份标示是 ${sessionId} ,您一共要猜测 ${numberOfWordsToGuess} 个单词,每个单词限制猜测 ${numberOfGuessAllowedForEachWord} 次`);

        CONFIG.sessionId = sessionId;
        CONFIG.numberOfGuessAllowedForEachWord = numberOfGuessAllowedForEachWord;
        CONFIG.numberOfWordsToGuess = numberOfWordsToGuess;

    } else {
        logger.error(`启动游戏失败!!!失败原因:`, res.body);
    }

    await GiveMeAWord();

}

/**
 *　2. Give Me A Word
 * https://github.com/strikingly/strikingly-interview-test-instructions#2-give-me-a-word
 */
async function GiveMeAWord() {

    await checkArriveLimitWords();

    let postData = {
        sessionId: CONFIG.sessionId,
        action: CONFIG.action.nextWord
    };
    let res = await request({
        method: 'post',
        body: postData,
        json: true,
        url: CONFIG.requestUrl
    });
    let {body, ok}=res;
    if (ok == true) {
        let {data}=body;
        let {totalWordCount, word, wrongGuessCountOfCurrentWord}=data;
        CONFIG.currentGuessWord.totalWordCount = totalWordCount;
        CONFIG.currentGuessWord.word = word;
        CONFIG.currentGuessWord.wrongGuessCountOfCurrentWord = wrongGuessCountOfCurrentWord;
        await makeAGuess(CONFIG);
    } else {
        logger.error(`获取猜谜单词失败!!!失败原因:`, res.body);
    }
}

/**
 * 3.Make A Guess
 * https://github.com/strikingly/strikingly-interview-test-instructions#3-make-a-guess
 * 猜测当前的单词
 */
async function makeAGuess() {

    let isNeedNextWord=  checkNeedNextWord();

    if(isNeedNextWord){
        initCurrentGuessWordStatus();
        await GiveMeAWord();
    }else {
        let {currentGuessWord, numberOfGuessAllowedForEachWord}=CONFIG;
        let {wrongGuessCountOfCurrentWord, totalWordCount, word,currentGuessCount}=currentGuessWord;
        logger.info(`第${totalWordCount}个单词: 第${currentGuessCount}次猜测,单词内容是 ${word},已经猜错了${wrongGuessCountOfCurrentWord}次(${numberOfGuessAllowedForEachWord})`);

        let mostPossibleChar = getBestMatchChar();

        let postData = {
            sessionId: CONFIG.sessionId,
            action: CONFIG.action.guessWord,
            guess: mostPossibleChar
        };
        let res = await request({
            method: 'post',
            body: postData,
            json: true,
            url: CONFIG.requestUrl
        });

        //TODO 如果mostPossibleChar不在返回单词里面，去除含有mostPossibleChar的单词
        setCurrentGuessStatus(res);

        //如果字符没有猜对
        if(!checkGuessCorrect(mostPossibleChar)){
            CONFIG.currentGuessWord.alreadyConfirmWrongCharAr.push(mostPossibleChar);
        }

        //如果单词没有全部猜对，递归调用猜词方法;否则直接nextWord
        //如果正在猜测的错误次数超过允许猜测的最大次数,nextWorld
        if(checkNeedNextWord()||checkWordCotainAsterisks()){
            await makeAGuess();
        }else{
            loggerfile.info(`-----　第${totalWordCount}个单词猜测成功，答案是 ${CONFIG.currentGuessWord.word} ------`);
            await wordHasGuessedOperation();
        }
    }
}

/**
 * 检查发送的字符是否正确
 */
function checkGuessCorrect(mostPossibleChar) {
    let {currentGuessWord}=CONFIG;
    let { word}=currentGuessWord;
    return word.includes(mostPossibleChar)
}

/**
 * 是否需要下一个单词 ,默认走正常猜词流程
 */
function checkNeedNextWord() {
    let flag=false;
    let {currentGuessWord, numberOfGuessAllowedForEachWord}=CONFIG;
    let {wrongGuessCountOfCurrentWord, totalWordCount,currentGuessCount}=currentGuessWord;

    //如果正在猜测的错误次数超过允许猜测的最大次数 或者　超过20次
    if (wrongGuessCountOfCurrentWord == numberOfGuessAllowedForEachWord) {
        loggerfile.info(`第${totalWordCount}个单词: 第${currentGuessCount}次猜测, 猜错次数已经达到默认上限 ${numberOfGuessAllowedForEachWord} ，开始下一轮.......failing`);
        flag=true;
    }else if(currentGuessCount>20){
        loggerfile.info(`第${totalWordCount}个单词: 第${currentGuessCount}次猜测, 猜测次数已经超过 20　 ，开始下一轮.......failing`);
        flag=true;
    }
    return flag;
}

async function checkArriveLimitWords() {
    let {currentGuessWord,numberOfWordsToGuess}=CONFIG;
    let {totalWordCount}=currentGuessWord;
    //如果正在猜的单词数等于一共要猜测的单词数量，GAME OVER,查询分数
    if(totalWordCount>numberOfWordsToGuess){
        await GetYourResult();
        processExit();
        return;
    }
}

/**
 * 如果猜中了单词，一些后续操作
 * -　从整体的单词库里面删除这个单词
 *
 */
async function wordHasGuessedOperation() {
    let {allWordsArray,wordsHasGuessed,currentGuessWord}=CONFIG;
    let {word}=currentGuessWord;
    CONFIG.wordsHasGuessed.push(word);

    //allWordsArray 里面删除这个单词
    let positiion=allWordsArray.indexOf(word);
    if(positiion!=-1){
        CONFIG.allWordsArray.splice(positiion,1);
        loggerfile.info(`-----　删除 ${word} 后剩余 ${CONFIG.allWordsArray.length}　个单词　------`);
    }
    initCurrentGuessWordStatus();
    await GiveMeAWord();
}

/**
 *
 * 4. Get Your Result
 */
async function GetYourResult(){
    let postData = {
        sessionId: CONFIG.sessionId,
        action: CONFIG.action.getResult
    };
    let res = await request({
        method: 'post',
        body: postData,
        json: true,
        url: CONFIG.requestUrl
    });
    loggerfile.info(`最后得分`,res);
}

function processExit() {
    loggerfile.info(`主动退出程序，游戏结束,之后pm2会自动重启,然后开启新一轮的游戏`);
    //删除data.json
    let {userDataPath}=configPath;
    rmFileSync(userDataPath);
}

/**
 * 初始化当前猜词状态
 */
function initCurrentGuessWordStatus() {
    let {currentGuessWord}=CONFIG;
    currentGuessWord.word=null;
    currentGuessWord.alreadyConfirmWrongCharAr=[];
    currentGuessWord.currentGuessCount=1;
    currentGuessWord.alreadyRequestCharAr=[];
    currentGuessWord.alreadyConfirmWrongCharAr=[];
    //重点是这个猜词的个数+1
    currentGuessWord.totalWordCount=++currentGuessWord.totalWordCount;
    currentGuessWord.wrongGuessCountOfCurrentWord=0;
}

/**
 * 判断单词是否含有*号
 */
function checkWordCotainAsterisks() {
    let {currentGuessWord}=CONFIG;
    let { word}=currentGuessWord;
    return word.includes('*');
}

/**
 * 判断单词中是否全部都是*号
 */
function checkWordAllAsterisks() {
    let {currentGuessWord}=CONFIG;
    let { word}=currentGuessWord;
    return word.split('').every((v)=>{
        return v=='*'
    })
}

/**
 * 针对不同的响应，更新当前的猜词状态
 * @param res
 */
function setCurrentGuessStatus(res) {
    let {body, ok}=res;
    let {data}=body;

    let {totalWordCount, word, wrongGuessCountOfCurrentWord}=data;
    if (ok == true) {
        CONFIG.currentGuessWord.totalWordCount = totalWordCount;
        CONFIG.currentGuessWord.word = word;
        CONFIG.currentGuessWord.wrongGuessCountOfCurrentWord = wrongGuessCountOfCurrentWord;
        CONFIG.currentGuessWord.currentGuessCount = ++CONFIG.currentGuessWord.currentGuessCount;
    }
}

/**
 * 思路：先获取单词的length ,只过滤剩下掉指定长度的word,然后这些word里面循环判断26个字母哪个字母的出现频率最高（同一个字母中重复字符忽略不计数）
 *
 * 获取最可能的字符
 */
function getBestMatchChar() {
    let {currentGuessWord, allWordsArray}=CONFIG;
    let {word, totalWordCount,alreadyRequestCharAr,alreadyConfirmWrongCharAr}=currentGuessWord;
    let wordLength = word.length;
    let allWordsArrayAfterSpecifyLenth = allWordsArray.filter((v)=> {
        return v.trim().length == wordLength && v.trim().indexOf("'") == -1;
    })
    logger.info(`第${totalWordCount}个单词: 先按照长度为 ${wordLength}　的英文单词过滤，过滤后共剩下 ${allWordsArrayAfterSpecifyLenth.length}个`);

    if(!checkWordAllAsterisks()){
        allWordsArrayAfterSpecifyLenth=allWordsArrayAfterSpecifyLenth.filter((v)=>{
            v=v.toUpperCase();
            let flag=true;
            word.split('').map((singleChar,k)=>{
                if(singleChar!='*'){
                    if(v[k]!=singleChar){
                        flag=false;
                    }
                }
            })
            return flag;
        })
        logger.info(`第${totalWordCount}个单词:　由于不是全*号，按照单词内容 ${word}　过滤, 过滤后共剩下 ${allWordsArrayAfterSpecifyLenth.length}个`);
    }

    if(alreadyConfirmWrongCharAr.length>0){
        allWordsArrayAfterSpecifyLenth=allWordsArrayAfterSpecifyLenth.filter((v)=>{
            let flag=true;
            if(checkStrContainEveryCharAr(v,alreadyConfirmWrongCharAr)){
                flag=false;
            }
            return flag;
        });
        logger.info(`第${totalWordCount}个单词:　排除掉含有 ${alreadyConfirmWrongCharAr.toString()} 的单词, 过滤后共剩下 ${allWordsArrayAfterSpecifyLenth.length}个`);
    }

    let objCount = {};
    allWordsArrayAfterSpecifyLenth.map((item)=> {
        let s = new Set();
        for (let i in item) {
            //如果是已经发送的猜测字符，那么下一次不继续发送了
            if(!alreadyRequestCharAr.includes(item[i].toUpperCase())){
                s.add(item[i]);
            }
        }
        for (let key of s.values()) {
            if (objCount[key]) {
                objCount[key] = (++objCount[key]);
            } else {
                objCount[key] = 1;
            }
        }
    })
    let maxKey;
    for (let i in objCount) {
        if (!maxKey) {
            maxKey = i;
        } else if (maxKey && objCount[maxKey] < objCount[i]) {
            maxKey = i;
        }
    }
    //TODO 如果没有匹配的字符，这里应该使用一个随机字符
    if(!maxKey){
        return 'E';
    }
    let mostPossibleChar = maxKey.toUpperCase();
    logger.info(`第${totalWordCount}个单词: 按照出现频率最高的字符是 ${mostPossibleChar} `);

    CONFIG.currentGuessWord.alreadyRequestCharAr.push(mostPossibleChar);

    return mostPossibleChar;
}

/**
 * 监测单词里面是否含有指定的字符数组.
 * 默认单词里面包含所有的字符数组
 */
function checkStrContainEveryCharAr(str,charAr) {
    let flag=true;
    charAr.map((char)=>{
        if(!str.includes(char)){
            flag=false;
        }
    })
    return flag;
}

function saveUserDataSync() {
    writeUserData(JSON.stringify(CONFIG,null,2));
}

main();
<|endoftext|>