<commit_before>import React from "react";
import withSideEffect from "react-side-effect";
import deepEqual from "deep-equal";
import objectAssign from "object-assign";
import {
    TAG_NAMES,
    TAG_PROPERTIES,
    REACT_TAG_MAP
} from "./HelmetConstants.js";
import PlainComponent from "./PlainComponent";

const HELMET_ATTRIBUTE = "data-react-helmet";

const encodeSpecialCharacters = (str) => {
    return String(str)
            .replace(/&/g, "&")
            .replace(/</g, "<")
            .replace(/>/g, ">")
            .replace(/"/g, """)
            .replace(/'/g, "'");
};

const getInnermostProperty = (propsList, property) => {
    const reversedPropsList = [].concat(propsList).reverse();

    for (let i = 0; i < reversedPropsList.length; i++) {
        const props = reversedPropsList[i];

        if (props[property]) {
            return props[property];
        }
    }

    return null;
};

const getTitleFromPropsList = (propsList) => {
    const innermostTitle = getInnermostProperty(propsList, "title");
    const innermostTemplate = getInnermostProperty(propsList, "titleTemplate");

    if (innermostTemplate && innermostTitle) {
        return innermostTemplate.replace(/\%s/g, innermostTitle);
    }

    const innermostDefaultTitle = getInnermostProperty(propsList, "defaultTitle");

    return innermostTitle || innermostDefaultTitle || "";
};

const getOnChangeClientState = (propsList) => {
    return getInnermostProperty(propsList, "onChangeClientState") ||(() => {});
};

const getHtmlAttributesFromPropsList = (propsList) => {
    return propsList
        .filter(props => typeof props[TAG_NAMES.HTML] !== "undefined")
        .map(props => props[TAG_NAMES.HTML])
        .reduce((html, current) => {
            return {...html, ...current};
        }, {});
};

const getBaseTagFromPropsList = (validTags, propsList) => {
    return propsList
        .filter(props => typeof props[TAG_NAMES.BASE] !== "undefined")
        .map(props => props[TAG_NAMES.BASE])
        .reverse()
        .reduce((innermostBaseTag, tag) => {
            if (!innermostBaseTag.length) {
                const keys = Object.keys(tag);

                for (let i = 0; i < keys.length; i++) {
                    const attributeKey = keys[i];
                    const lowerCaseAttributeKey = attributeKey.toLowerCase();

                    if (validTags.indexOf(lowerCaseAttributeKey) !== -1) {
                        return innermostBaseTag.concat(tag);
                    }
                }
            }

            return innermostBaseTag;
        }, []);
};

const getTagsFromPropsList = (tagName, validTags, propsList) => {
    // Calculate list of tags, giving priority innermost component (end of the propslist)
    const approvedSeenTags = {};

    const tagList = propsList
        .filter(props => typeof props[tagName] !== "undefined")
        .map(props => props[tagName])
        .reverse()
        .reduce((approvedTags, instanceTags) => {
            const instanceSeenTags = {};

            instanceTags.filter(tag => {
                let validAttributeKey;
                const keys = Object.keys(tag);
                for (let i = 0; i < keys.length; i++) {
                    const attributeKey = keys[i];
                    const lowerCaseAttributeKey = attributeKey.toLowerCase();

                    // Special rule with link tags, since rel and href are both valid tags, rel takes priority
                    if (validTags.indexOf(lowerCaseAttributeKey) !== -1
                        && !(validAttributeKey === TAG_PROPERTIES.REL && tag[validAttributeKey].toLowerCase() === "canonical")
                        && !(lowerCaseAttributeKey === TAG_PROPERTIES.REL && tag[lowerCaseAttributeKey].toLowerCase() === "stylesheet")) {
                        validAttributeKey = lowerCaseAttributeKey;
                    }
                    // Special case for innerHTML which doesn't work lowercased
                    if (validTags.indexOf(attributeKey) !== -1 && attributeKey === TAG_PROPERTIES.INNER_HTML) {
                        validAttributeKey = attributeKey;
                    }
                }

                if (!validAttributeKey) {
                    return false;
                }

                const value = tag[validAttributeKey].toLowerCase();

                if (!approvedSeenTags[validAttributeKey]) {
                    approvedSeenTags[validAttributeKey] = {};
                }

                if (!instanceSeenTags[validAttributeKey]) {
                    instanceSeenTags[validAttributeKey] = {};
                }

                if (!approvedSeenTags[validAttributeKey][value]) {
                    instanceSeenTags[validAttributeKey][value] = true;
                    return true;
                }

                return false;
            })
            .reverse()
            .forEach(tag => approvedTags.push(tag));

            // Update seen tags with tags from this instance
            const keys = Object.keys(instanceSeenTags);
            for (let i = 0; i < keys.length; i++) {
                const attributeKey = keys[i];
                const tagUnion = objectAssign(
                    {},
                    approvedSeenTags[attributeKey],
                    instanceSeenTags[attributeKey]
                );

                approvedSeenTags[attributeKey] = tagUnion;
            }

            return approvedTags;
        }, [])
        .reverse();

    return tagList;
};

const updateTitle = title => {
    document.title = title || document.title;
};

const updateHtmlAttributes = (attributes) => {
    const htmlTag = document.getElementsByTagName("html")[0];
    const helmetAttributeString = htmlTag.getAttribute(HELMET_ATTRIBUTE);
    const helmetAttributes = helmetAttributeString ? helmetAttributeString.split(",") : [];
    const attributesToRemove = [].concat(helmetAttributes);
    const attributeKeys = Object.keys(attributes);

    for (let i = 0; i < attributeKeys.length; i++) {
        const attribute = attributeKeys[i];
        const value = attributes[attribute] || "";
        htmlTag.setAttribute(attribute, value);

        if (helmetAttributes.indexOf(attribute) === -1) {
            helmetAttributes.push(attribute);
        }

        const indexToSave = attributesToRemove.indexOf(attribute);
        if (indexToSave !== -1) {
            attributesToRemove.splice(indexToSave, 1);
        }
    }

    for (let i = attributesToRemove.length - 1; i >= 0; i--) {
        htmlTag.removeAttribute(attributesToRemove[i]);
    }

    htmlTag.setAttribute(HELMET_ATTRIBUTE, helmetAttributes.join(","));
};

const updateTags = (type, tags) => {
    const headElement = document.head || document.querySelector("head");
    const tagNodes = headElement.querySelectorAll(`${type}[${HELMET_ATTRIBUTE}]`);
    const oldTags = Array.prototype.slice.call(tagNodes);
    const newTags = [];
    let indexToDelete;

    if (tags && tags.length) {
        tags
        .forEach(tag => {
            const newElement = document.createElement(type);

            for (const attribute in tag) {
                if (tag.hasOwnProperty(attribute)) {
                    if (attribute === "innerHTML") {
                        newElement.innerHTML = tag.innerHTML;
                    } else {
                        newElement.setAttribute(attribute, tag[attribute]);
                    }
                }
            }

            newElement.setAttribute(HELMET_ATTRIBUTE, "true");

            // Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
            if (oldTags.some((existingTag, index) => {
                indexToDelete = index;
                return newElement.isEqualNode(existingTag);
            })) {
                oldTags.splice(indexToDelete, 1);
            } else {
                newTags.push(newElement);
            }
        });
    }

    oldTags.forEach(tag => tag.parentNode.removeChild(tag));
    newTags.forEach(tag => headElement.appendChild(tag));

    return {
        oldTags,
        newTags
    };
};

const generateHtmlAttributesAsString = (attributes) => {
    const keys = Object.keys(attributes);
    let attributeString = "";

    for (let i = 0; i < keys.length; i++) {
        const attribute = keys[i];
        const attr = typeof attributes[attribute] !== "undefined" ? `${attribute}="${attributes[attribute]}"` : `${attribute}`;
        attributeString += `${attr} `;
    }

    return attributeString.trim();
};

const generateTitleAsString = (type, title) => {
    const stringifiedMarkup = `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(title)}</${type}>`;

    return stringifiedMarkup;
};

const generateTagsAsString = (type, tags) => {
    const stringifiedMarkup = tags.map(tag => {
        const attributeHtml = Object.keys(tag)
            .map((attribute) => {
                if (attribute === "innerHTML") {
                    return "";
                }

                if (typeof tag[attribute] === "undefined") {
                    return attribute;
                }

                const encodedValue = encodeSpecialCharacters(tag[attribute]);
                return `${attribute}="${encodedValue}"`;
            })
            .join(" ");

        const innerHTML = tag.innerHTML || "";

        return `<${type} ${HELMET_ATTRIBUTE}="true" ${attributeHtml}${type === TAG_NAMES.SCRIPT ? `>${innerHTML}</${type}>` : `/>`}`;
    }).join("");

    return stringifiedMarkup;
};

const generateTitleAsReactComponent = (type, title) => {
    // assigning into an array to define toString function on it
    const component = [
        React.createElement(
            TAG_NAMES.TITLE,
            {
                key: title,
                [HELMET_ATTRIBUTE]: true
            },
            title
        )
    ];

    return component;
};

const generateTagsAsReactComponent = (type, tags) => {
    /* eslint-disable react/display-name */
    const component = tags.map((tag, i) => {
        const mappedTag = {
            key: i,
            [HELMET_ATTRIBUTE]: true
        };

        Object.keys(tag).forEach((attribute) => {
            const mappedAttribute = REACT_TAG_MAP[attribute] || attribute;

            if (mappedAttribute === "innerHTML") {
                mappedTag.dangerouslySetInnerHTML = {__html: tag.innerHTML};
            } else {
                mappedTag[mappedAttribute] = tag[attribute];
            }
        });

        return React.createElement(type, mappedTag);
    });

    return component;
    /* eslint-enable react/display-name */
};

const getMethodsForTag = (type, tags) => {
    switch (type) {
        case TAG_NAMES.TITLE:
            return {
                toComponent: () => generateTitleAsReactComponent(type, tags),
                toString: () => generateTitleAsString(type, tags)
            };
        case TAG_NAMES.HTML:
            return {
                toComponent: () => tags,
                toString: () => generateHtmlAttributesAsString(tags)
            };
        default:
            return {
                toComponent: () => generateTagsAsReactComponent(type, tags),
                toString: () => generateTagsAsString(type, tags)
            };
    }
};

const mapStateOnServer = ({title, baseTag, metaTags, linkTags, scriptTags, htmlAttributes}) => ({
    title: getMethodsForTag(TAG_NAMES.TITLE, title),
    base: getMethodsForTag(TAG_NAMES.BASE, baseTag),
    meta: getMethodsForTag(TAG_NAMES.META, metaTags),
    link: getMethodsForTag(TAG_NAMES.LINK, linkTags),
    script: getMethodsForTag(TAG_NAMES.SCRIPT, scriptTags),
    htmlAttributes: getMethodsForTag(TAG_NAMES.HTML, htmlAttributes)
});

const Helmet = (Component) => {
    /* eslint-disable react/no-multi-comp */
    class HelmetWrapper extends React.Component {
        /**
         * @param {String} title: "Title"
         * @param {String} defaultTitle: "Default Title"
         * @param {Function} onChangeClientState: "(newState) => console.log(newState)"
         * @param {String} titleTemplate: "MySite.com - %s"
         * @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"}
         * @param {Array} meta: [{"name": "description", "content": "Test description"}]
         * @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}]
         * @param {Array} script: [{"src": "http://mysite.com/js/test.js", "type": "text/javascript"}]
         * @param {Object} htmlAttributes: {"lang": "en", "amp": undefined}
         */
        static propTypes = {
            title: React.PropTypes.string,
            defaultTitle: React.PropTypes.string,
            onChangeClientState: React.PropTypes.func,
            titleTemplate: React.PropTypes.string,
            base: React.PropTypes.object,
            meta: React.PropTypes.arrayOf(React.PropTypes.object),
            link: React.PropTypes.arrayOf(React.PropTypes.object),
            script: React.PropTypes.arrayOf(React.PropTypes.object),
            htmlAttributes: React.PropTypes.object
        }

        shouldComponentUpdate(nextProps) {
            return !deepEqual(this.props, nextProps);
        }

        // Component.peak comes from react-side-effect:
        // For testing, you may use a static peek() method available on the returned component.
        // It lets you get the current state without resetting the mounted instance stack.
        // Don’t use it for anything other than testing.
        static peek = Component.peek
        static rewind = () => {
            let mappedState = Component.rewind();
            if (!mappedState) {
                // provide fallback if mappedState is undefined
                mappedState = mapStateOnServer({
                    title: "",
                    baseTag: [],
                    metaTags: [],
                    linkTags: [],
                    scriptTags: [],
                    htmlAttributes: []
                });
            }

            return mappedState;
        }

        static set canUseDOM(canUseDOM) {
            Component.canUseDOM = canUseDOM;
        }

        render() {
            return <Component {...this.props} />;
        }
    }
    /* eslint-enable react/no-multi-comp */

    return HelmetWrapper;
};

const reducePropsToState = (propsList) => ({
    title: getTitleFromPropsList(propsList),
    onChangeClientState: getOnChangeClientState(propsList),
    baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
    metaTags: getTagsFromPropsList(TAG_NAMES.META, [TAG_PROPERTIES.NAME, TAG_PROPERTIES.CHARSET, TAG_PROPERTIES.HTTPEQUIV, TAG_PROPERTIES.PROPERTY], propsList),
    linkTags: getTagsFromPropsList(TAG_NAMES.LINK, [TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], propsList),
    scriptTags: getTagsFromPropsList(TAG_NAMES.SCRIPT, [TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML, TAG_PROPERTIES.NAME], propsList),
    htmlAttributes: getHtmlAttributesFromPropsList(propsList)
});

const handleClientStateChange = (newState) => {
    const {title, htmlAttributes, baseTag, metaTags, linkTags, scriptTags, onChangeClientState} = newState;

    updateTitle(title);

    updateHtmlAttributes(htmlAttributes);

    const tagUpdates = {
        scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags),
        linkTags: updateTags(TAG_NAMES.LINK, linkTags),
        metaTags: updateTags(TAG_NAMES.META, metaTags),
        baseTag: updateTags(TAG_NAMES.BASE, baseTag)
    };

    const addedTags = {};
    const removedTags = {};

    Object.keys(tagUpdates).forEach(tagType => {
        const {newTags, oldTags} = tagUpdates[tagType];

        if (newTags.length) {
            addedTags[tagType] = newTags;
        }
        if (oldTags.length) {
            removedTags[tagType] = tagUpdates[tagType].oldTags;
        }
    });

    onChangeClientState(newState, addedTags, removedTags);
};

const SideEffect = withSideEffect(
    reducePropsToState,
    handleClientStateChange,
    mapStateOnServer
);

// PlainComponent is used to be a blank component decorated by react-side-effect
export default Helmet(SideEffect(PlainComponent));
<commit_msg>fix(react-helmet): Render empty string on the client<commit_after>import React from "react";
import withSideEffect from "react-side-effect";
import deepEqual from "deep-equal";
import objectAssign from "object-assign";
import {
    TAG_NAMES,
    TAG_PROPERTIES,
    REACT_TAG_MAP
} from "./HelmetConstants.js";
import PlainComponent from "./PlainComponent";

const HELMET_ATTRIBUTE = "data-react-helmet";

const encodeSpecialCharacters = (str) => {
    return String(str)
            .replace(/&/g, "&")
            .replace(/</g, "<")
            .replace(/>/g, ">")
            .replace(/"/g, """)
            .replace(/'/g, "'");
};

const getInnermostProperty = (propsList, property) => {
    const reversedPropsList = [].concat(propsList).reverse();

    for (let i = 0; i < reversedPropsList.length; i++) {
        const props = reversedPropsList[i];

        if (props[property]) {
            return props[property];
        }
    }

    return null;
};

const getTitleFromPropsList = (propsList) => {
    const innermostTitle = getInnermostProperty(propsList, "title");
    const innermostTemplate = getInnermostProperty(propsList, "titleTemplate");

    if (innermostTemplate && innermostTitle) {
        return innermostTemplate.replace(/\%s/g, innermostTitle);
    }

    const innermostDefaultTitle = getInnermostProperty(propsList, "defaultTitle");

    return innermostTitle || innermostDefaultTitle || "";
};

const getOnChangeClientState = (propsList) => {
    return getInnermostProperty(propsList, "onChangeClientState") ||(() => {});
};

const getHtmlAttributesFromPropsList = (propsList) => {
    return propsList
        .filter(props => typeof props[TAG_NAMES.HTML] !== "undefined")
        .map(props => props[TAG_NAMES.HTML])
        .reduce((html, current) => {
            return {...html, ...current};
        }, {});
};

const getBaseTagFromPropsList = (validTags, propsList) => {
    return propsList
        .filter(props => typeof props[TAG_NAMES.BASE] !== "undefined")
        .map(props => props[TAG_NAMES.BASE])
        .reverse()
        .reduce((innermostBaseTag, tag) => {
            if (!innermostBaseTag.length) {
                const keys = Object.keys(tag);

                for (let i = 0; i < keys.length; i++) {
                    const attributeKey = keys[i];
                    const lowerCaseAttributeKey = attributeKey.toLowerCase();

                    if (validTags.indexOf(lowerCaseAttributeKey) !== -1) {
                        return innermostBaseTag.concat(tag);
                    }
                }
            }

            return innermostBaseTag;
        }, []);
};

const getTagsFromPropsList = (tagName, validTags, propsList) => {
    // Calculate list of tags, giving priority innermost component (end of the propslist)
    const approvedSeenTags = {};

    const tagList = propsList
        .filter(props => typeof props[tagName] !== "undefined")
        .map(props => props[tagName])
        .reverse()
        .reduce((approvedTags, instanceTags) => {
            const instanceSeenTags = {};

            instanceTags.filter(tag => {
                let validAttributeKey;
                const keys = Object.keys(tag);
                for (let i = 0; i < keys.length; i++) {
                    const attributeKey = keys[i];
                    const lowerCaseAttributeKey = attributeKey.toLowerCase();

                    // Special rule with link tags, since rel and href are both valid tags, rel takes priority
                    if (validTags.indexOf(lowerCaseAttributeKey) !== -1
                        && !(validAttributeKey === TAG_PROPERTIES.REL && tag[validAttributeKey].toLowerCase() === "canonical")
                        && !(lowerCaseAttributeKey === TAG_PROPERTIES.REL && tag[lowerCaseAttributeKey].toLowerCase() === "stylesheet")) {
                        validAttributeKey = lowerCaseAttributeKey;
                    }
                    // Special case for innerHTML which doesn't work lowercased
                    if (validTags.indexOf(attributeKey) !== -1 && attributeKey === TAG_PROPERTIES.INNER_HTML) {
                        validAttributeKey = attributeKey;
                    }
                }

                if (!validAttributeKey) {
                    return false;
                }

                const value = tag[validAttributeKey].toLowerCase();

                if (!approvedSeenTags[validAttributeKey]) {
                    approvedSeenTags[validAttributeKey] = {};
                }

                if (!instanceSeenTags[validAttributeKey]) {
                    instanceSeenTags[validAttributeKey] = {};
                }

                if (!approvedSeenTags[validAttributeKey][value]) {
                    instanceSeenTags[validAttributeKey][value] = true;
                    return true;
                }

                return false;
            })
            .reverse()
            .forEach(tag => approvedTags.push(tag));

            // Update seen tags with tags from this instance
            const keys = Object.keys(instanceSeenTags);
            for (let i = 0; i < keys.length; i++) {
                const attributeKey = keys[i];
                const tagUnion = objectAssign(
                    {},
                    approvedSeenTags[attributeKey],
                    instanceSeenTags[attributeKey]
                );

                approvedSeenTags[attributeKey] = tagUnion;
            }

            return approvedTags;
        }, [])
        .reverse();

    return tagList;
};

const updateTitle = title => {
    document.title = title || document.title;
};

const updateHtmlAttributes = (attributes) => {
    const htmlTag = document.getElementsByTagName("html")[0];
    const helmetAttributeString = htmlTag.getAttribute(HELMET_ATTRIBUTE);
    const helmetAttributes = helmetAttributeString ? helmetAttributeString.split(",") : [];
    const attributesToRemove = [].concat(helmetAttributes);
    const attributeKeys = Object.keys(attributes);

    for (let i = 0; i < attributeKeys.length; i++) {
        const attribute = attributeKeys[i];
        const value = attributes[attribute] || "";
        htmlTag.setAttribute(attribute, value);

        if (helmetAttributes.indexOf(attribute) === -1) {
            helmetAttributes.push(attribute);
        }

        const indexToSave = attributesToRemove.indexOf(attribute);
        if (indexToSave !== -1) {
            attributesToRemove.splice(indexToSave, 1);
        }
    }

    for (let i = attributesToRemove.length - 1; i >= 0; i--) {
        htmlTag.removeAttribute(attributesToRemove[i]);
    }

    htmlTag.setAttribute(HELMET_ATTRIBUTE, helmetAttributes.join(","));
};

const updateTags = (type, tags) => {
    const headElement = document.head || document.querySelector("head");
    const tagNodes = headElement.querySelectorAll(`${type}[${HELMET_ATTRIBUTE}]`);
    const oldTags = Array.prototype.slice.call(tagNodes);
    const newTags = [];
    let indexToDelete;

    if (tags && tags.length) {
        tags
        .forEach(tag => {
            const newElement = document.createElement(type);

            for (const attribute in tag) {
                if (tag.hasOwnProperty(attribute)) {
                    if (attribute === "innerHTML") {
                        newElement.innerHTML = tag.innerHTML;
                    } else {
                        const value = (typeof tag[attribute] === "undefined") ? "" : tag[attribute];
                        newElement.setAttribute(attribute, value);
                    }
                }
            }

            newElement.setAttribute(HELMET_ATTRIBUTE, "true");

            // Remove a duplicate tag from domTagstoRemove, so it isn't cleared.
            if (oldTags.some((existingTag, index) => {
                indexToDelete = index;
                return newElement.isEqualNode(existingTag);
            })) {
                oldTags.splice(indexToDelete, 1);
            } else {
                newTags.push(newElement);
            }
        });
    }

    oldTags.forEach(tag => tag.parentNode.removeChild(tag));
    newTags.forEach(tag => headElement.appendChild(tag));

    return {
        oldTags,
        newTags
    };
};

const generateHtmlAttributesAsString = (attributes) => {
    const keys = Object.keys(attributes);
    let attributeString = "";

    for (let i = 0; i < keys.length; i++) {
        const attribute = keys[i];
        const attr = typeof attributes[attribute] !== "undefined" ? `${attribute}="${attributes[attribute]}"` : `${attribute}`;
        attributeString += `${attr} `;
    }

    return attributeString.trim();
};

const generateTitleAsString = (type, title) => {
    const stringifiedMarkup = `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(title)}</${type}>`;

    return stringifiedMarkup;
};

const generateTagsAsString = (type, tags) => {
    const stringifiedMarkup = tags.map(tag => {
        const attributeHtml = Object.keys(tag)
            .map((attribute) => {
                if (attribute === "innerHTML") {
                    return "";
                }

                if (typeof tag[attribute] === "undefined") {
                    return attribute;
                }

                const encodedValue = encodeSpecialCharacters(tag[attribute]);
                return `${attribute}="${encodedValue}"`;
            })
            .join(" ").trim();

        const innerHTML = tag.innerHTML || "";

        return `<${type} ${HELMET_ATTRIBUTE}="true" ${attributeHtml}${type === TAG_NAMES.SCRIPT ? `>${innerHTML}</${type}>` : `/>`}`;
    }).join("");

    return stringifiedMarkup;
};

const generateTitleAsReactComponent = (type, title) => {
    // assigning into an array to define toString function on it
    const component = [
        React.createElement(
            TAG_NAMES.TITLE,
            {
                key: title,
                [HELMET_ATTRIBUTE]: true
            },
            title
        )
    ];

    return component;
};

const generateTagsAsReactComponent = (type, tags) => {
    /* eslint-disable react/display-name */
    const component = tags.map((tag, i) => {
        const mappedTag = {
            key: i,
            [HELMET_ATTRIBUTE]: true
        };

        Object.keys(tag).forEach((attribute) => {
            const mappedAttribute = REACT_TAG_MAP[attribute] || attribute;

            if (mappedAttribute === "innerHTML") {
                mappedTag.dangerouslySetInnerHTML = {__html: tag.innerHTML};
            } else {
                mappedTag[mappedAttribute] = tag[attribute];
            }
        });

        return React.createElement(type, mappedTag);
    });

    return component;
    /* eslint-enable react/display-name */
};

const getMethodsForTag = (type, tags) => {
    switch (type) {
        case TAG_NAMES.TITLE:
            return {
                toComponent: () => generateTitleAsReactComponent(type, tags),
                toString: () => generateTitleAsString(type, tags)
            };
        case TAG_NAMES.HTML:
            return {
                toComponent: () => tags,
                toString: () => generateHtmlAttributesAsString(tags)
            };
        default:
            return {
                toComponent: () => generateTagsAsReactComponent(type, tags),
                toString: () => generateTagsAsString(type, tags)
            };
    }
};

const mapStateOnServer = ({title, baseTag, metaTags, linkTags, scriptTags, htmlAttributes}) => ({
    title: getMethodsForTag(TAG_NAMES.TITLE, title),
    base: getMethodsForTag(TAG_NAMES.BASE, baseTag),
    meta: getMethodsForTag(TAG_NAMES.META, metaTags),
    link: getMethodsForTag(TAG_NAMES.LINK, linkTags),
    script: getMethodsForTag(TAG_NAMES.SCRIPT, scriptTags),
    htmlAttributes: getMethodsForTag(TAG_NAMES.HTML, htmlAttributes)
});

const Helmet = (Component) => {
    /* eslint-disable react/no-multi-comp */
    class HelmetWrapper extends React.Component {
        /**
         * @param {String} title: "Title"
         * @param {String} defaultTitle: "Default Title"
         * @param {Function} onChangeClientState: "(newState) => console.log(newState)"
         * @param {String} titleTemplate: "MySite.com - %s"
         * @param {Object} base: {"target": "_blank", "href": "http://mysite.com/"}
         * @param {Array} meta: [{"name": "description", "content": "Test description"}]
         * @param {Array} link: [{"rel": "canonical", "href": "http://mysite.com/example"}]
         * @param {Array} script: [{"src": "http://mysite.com/js/test.js", "type": "text/javascript"}]
         * @param {Object} htmlAttributes: {"lang": "en", "amp": undefined}
         */
        static propTypes = {
            title: React.PropTypes.string,
            defaultTitle: React.PropTypes.string,
            onChangeClientState: React.PropTypes.func,
            titleTemplate: React.PropTypes.string,
            base: React.PropTypes.object,
            meta: React.PropTypes.arrayOf(React.PropTypes.object),
            link: React.PropTypes.arrayOf(React.PropTypes.object),
            script: React.PropTypes.arrayOf(React.PropTypes.object),
            htmlAttributes: React.PropTypes.object
        }

        shouldComponentUpdate(nextProps) {
            return !deepEqual(this.props, nextProps);
        }

        // Component.peak comes from react-side-effect:
        // For testing, you may use a static peek() method available on the returned component.
        // It lets you get the current state without resetting the mounted instance stack.
        // Don’t use it for anything other than testing.
        static peek = Component.peek
        static rewind = () => {
            let mappedState = Component.rewind();
            if (!mappedState) {
                // provide fallback if mappedState is undefined
                mappedState = mapStateOnServer({
                    title: "",
                    baseTag: [],
                    metaTags: [],
                    linkTags: [],
                    scriptTags: [],
                    htmlAttributes: []
                });
            }

            return mappedState;
        }

        static set canUseDOM(canUseDOM) {
            Component.canUseDOM = canUseDOM;
        }

        render() {
            return <Component {...this.props} />;
        }
    }
    /* eslint-enable react/no-multi-comp */

    return HelmetWrapper;
};

const reducePropsToState = (propsList) => ({
    title: getTitleFromPropsList(propsList),
    onChangeClientState: getOnChangeClientState(propsList),
    baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
    metaTags: getTagsFromPropsList(TAG_NAMES.META, [TAG_PROPERTIES.NAME, TAG_PROPERTIES.CHARSET, TAG_PROPERTIES.HTTPEQUIV, TAG_PROPERTIES.PROPERTY], propsList),
    linkTags: getTagsFromPropsList(TAG_NAMES.LINK, [TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF], propsList),
    scriptTags: getTagsFromPropsList(TAG_NAMES.SCRIPT, [TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML, TAG_PROPERTIES.NAME], propsList),
    htmlAttributes: getHtmlAttributesFromPropsList(propsList)
});

const handleClientStateChange = (newState) => {
    const {title, htmlAttributes, baseTag, metaTags, linkTags, scriptTags, onChangeClientState} = newState;

    updateTitle(title);

    updateHtmlAttributes(htmlAttributes);

    const tagUpdates = {
        scriptTags: updateTags(TAG_NAMES.SCRIPT, scriptTags),
        linkTags: updateTags(TAG_NAMES.LINK, linkTags),
        metaTags: updateTags(TAG_NAMES.META, metaTags),
        baseTag: updateTags(TAG_NAMES.BASE, baseTag)
    };

    const addedTags = {};
    const removedTags = {};

    Object.keys(tagUpdates).forEach(tagType => {
        const {newTags, oldTags} = tagUpdates[tagType];

        if (newTags.length) {
            addedTags[tagType] = newTags;
        }
        if (oldTags.length) {
            removedTags[tagType] = tagUpdates[tagType].oldTags;
        }
    });

    onChangeClientState(newState, addedTags, removedTags);
};

const SideEffect = withSideEffect(
    reducePropsToState,
    handleClientStateChange,
    mapStateOnServer
);

// PlainComponent is used to be a blank component decorated by react-side-effect
export default Helmet(SideEffect(PlainComponent));
<|endoftext|>


<commit_before>'use strict';

angular.module('app').controller('quizzController', function ($scope, $stateParams, $interval, quizz, config) {

    var type = $stateParams.type || 'single';

    /*
     * Load Current ID Blind
     */
    quizz.blindStart(type).then(function(id){
        $scope.blindId = id;
    });

    /*
     * Load questions Single/Multi
     */
    quizz.loadQuestions(type).then(function (result) {
        $scope.questions = result.questions;
    });

    /*
     * Quizz Start at Question 0 !
     */
    $scope.currentQuestion = 0;

    /*
     * Get PATH for img Url
     */
    $scope.path = config.apiUrl;

    /*
     * Content all responses for all questions
     */
    $scope.responses = quizz.emptyResponses();

    /*
     * Timer
     */
    $scope.countDown = config.timeMusic;
    $scope.percentCountDown = ($scope.countDown * 100) / config.timeMusic;

    $scope.$watch('countDown', function(newVal){
        $scope.percentCountDown = (newVal * 100) / config.timeMusic;
    });

    var timer = $interval(function(){
        $scope.countDown--;
    }, 1000);

    $scope.$watch('countDown', function(newVal){
       if(newVal == 0){
           $interval.cancel(timer);
           $scope.newQuestion(1, 1);
           console.log('Done');
       }
    });

    /*
     * Next Questions
     * TODO Move to Service
     */
    $scope.newQuestion = function (index, endTimer) {

        $interval.cancel(timer);
        $scope.countDown = config.timeMusic;

        var endTimer = endTimer || null;
        var question = $scope.questions[$scope.currentQuestion];

        console.log(question);

        if(endTimer == 1){
            $scope.responses[$scope.currentQuestion].value = 0;
            quizz.addResponse($scope.blindId, question.question.id);
            console.log('[QUIZZ] : Bad answer');
        }
        else{
            /* Good Answer with current Question */
            if (question.anwsers[index].status == 1) {
                $scope.responses[$scope.currentQuestion].value = 1;
                console.log('[QUIZZ] : Good answer');
            }
            else{
                $scope.responses[$scope.currentQuestion].value = 0;
                console.log('[QUIZZ] : Bad answer');
            }
            quizz.addResponse($scope.blindId, question.question.id, question.anwsers[index].id);
        }
        /* Check if last question */
        if($scope.currentQuestion == 19){
            $interval.cancel(timer);
            console.log('[QUIZZ] : Challenge '+type+' done');
            if(type === 'single'){
                /*
                 * Create Algo for now what category is
                 * Asign category to the player in session & API WAY
                 */
            }
            else{
                /*
                 * Save response in API WAY
                 * Seed Notification to the second player.
                 */
            }
        }
        else{
            timer = $interval(function(){
                $scope.countDown--;
            }, 1000);
            $scope.currentQuestion++;
            console.log('[QUIZZ] : Next question : '+$scope.currentQuestion);
        }
    };

});<commit_msg>Question logged for Quizz<commit_after>'use strict';

angular.module('app').controller('quizzController', function ($scope, $stateParams, $state, $interval, quizz, config, session) {

    var type = $stateParams.type || 'single';

    /*
     * If we are not logged -> redirect to home
     */
    if(session.getLogged() === false){
        $state.go('home');
    }

    /*
     * Load Current ID Blind
     */
    quizz.blindStart(type).then(function(id){
        $scope.blindId = id;
    });

    /*
     * Load questions Single/Multi
     */
    quizz.loadQuestions(type).then(function (result) {
        $scope.questions = result.questions;
    });

    /*
     * Quizz Start at Question 0 !
     */
    $scope.currentQuestion = 0;

    /*
     * Get PATH for img Url
     */
    $scope.path = config.apiUrl;

    /*
     * Content all responses for all questions
     */
    $scope.responses = quizz.emptyResponses();

    /*
     * Timer
     */
    $scope.countDown = config.timeMusic;
    $scope.percentCountDown = ($scope.countDown * 100) / config.timeMusic;

    $scope.$watch('countDown', function(newVal){
        $scope.percentCountDown = (newVal * 100) / config.timeMusic;
    });

    var timer = $interval(function(){
        $scope.countDown--;
    }, 1000);

    $scope.$watch('countDown', function(newVal){
       if(newVal == 0){
           $interval.cancel(timer);
           $scope.newQuestion(1, 1);
           console.log('Done');
       }
    });

    /*
     * Next Questions
     * TODO Move to Service
     */
    $scope.newQuestion = function (index, endTimer) {

        $interval.cancel(timer);
        $scope.countDown = config.timeMusic;

        var endTimer = endTimer || null;
        var question = $scope.questions[$scope.currentQuestion];

        console.log(question);

        if(endTimer == 1){
            $scope.responses[$scope.currentQuestion].value = 0;
            quizz.addResponse($scope.blindId, question.question.id);
            console.log('[QUIZZ] : Bad answer');
        }
        else{
            /* Good Answer with current Question */
            if (question.anwsers[index].status == 1) {
                $scope.responses[$scope.currentQuestion].value = 1;
                console.log('[QUIZZ] : Good answer');
            }
            else{
                $scope.responses[$scope.currentQuestion].value = 0;
                console.log('[QUIZZ] : Bad answer');
            }
            quizz.addResponse($scope.blindId, question.question.id, question.anwsers[index].id);
        }
        /* Check if last question */
        if($scope.currentQuestion == 19){
            $interval.cancel(timer);
            console.log('[QUIZZ] : Challenge '+type+' done');
            if(type === 'single'){
                /*
                 * Create Algo for now what category is
                 * Asign category to the player in session & API WAY
                 */
            }
            else{
                /*
                 * Save response in API WAY
                 * Seed Notification to the second player.
                 */
            }
        }
        else{
            timer = $interval(function(){
                $scope.countDown--;
            }, 1000);
            $scope.currentQuestion++;
            console.log('[QUIZZ] : Next question : '+$scope.currentQuestion);
        }
    };

});<|endoftext|>


<commit_before>'use strict';

const safeBuffer = require('safe-buffer');
const benchmark = require('benchmark');
const crypto = require('crypto');

const WebSocket = require('..');

const Receiver = WebSocket.Receiver;
const Sender = WebSocket.Sender;
const Buffer = safeBuffer.Buffer;

//
// Override the `cleanup` method to make the "close message" test work as
// expected.
//
Receiver.prototype.cleanup = function () {
  this._state = 0;
};

const options = {
  fin: true,
  rsv1: false,
  mask: true,
  readOnly: false
};

function createBinaryFrame (length) {
  const list = Sender.frame(
    crypto.randomBytes(length),
    Object.assign({ opcode: 0x02 }, options)
  );

  return Buffer.concat(list);
}

const pingFrame1 = Buffer.concat(Sender.frame(
  crypto.randomBytes(5),
  Object.assign({ opcode: 0x09 }, options)
));

const textFrame = Buffer.from('819461616161' + '61'.repeat(20), 'hex');
const pingFrame2 = Buffer.from('8900', 'hex');
const closeFrame = Buffer.from('8800', 'hex');
const binaryFrame1 = createBinaryFrame(125);
const binaryFrame2 = createBinaryFrame(65535);
const binaryFrame3 = createBinaryFrame(200 * 1024);
const binaryFrame4 = createBinaryFrame(1024 * 1024);

const suite = new benchmark.Suite();
const receiver = new Receiver();

receiver.onmessage = receiver.onclose = receiver.onping = () => {};

suite.add('ping frame (5 bytes payload)', () => receiver.add(pingFrame1));
suite.add('ping frame (no payload)', () => receiver.add(pingFrame2));
suite.add('close frame (no payload)', () => receiver.add(closeFrame));
suite.add('text frame (20 bytes payload)', () => receiver.add(textFrame));
suite.add('binary frame (125 bytes payload)', () => receiver.add(binaryFrame1));
suite.add('binary frame (65535 bytes payload)', () => receiver.add(binaryFrame2));
suite.add('binary frame (200 KiB payload)', () => receiver.add(binaryFrame3));
suite.add('binary frame (1 MiB payload)', () => receiver.add(binaryFrame4));

suite.on('cycle', (e) => console.log(e.target.toString()));

if (require.main === module) {
  suite.run({ async: true });
} else {
  module.exports = suite;
}
<commit_msg>[benchmark] Fix parser benchmark<commit_after>'use strict';

const safeBuffer = require('safe-buffer');
const benchmark = require('benchmark');
const crypto = require('crypto');

const WebSocket = require('..');

const Receiver = WebSocket.Receiver;
const Sender = WebSocket.Sender;
const Buffer = safeBuffer.Buffer;

const options = {
  fin: true,
  rsv1: false,
  mask: true,
  readOnly: false
};

function createBinaryFrame (length) {
  const list = Sender.frame(
    crypto.randomBytes(length),
    Object.assign({ opcode: 0x02 }, options)
  );

  return Buffer.concat(list);
}

const pingFrame1 = Buffer.concat(Sender.frame(
  crypto.randomBytes(5),
  Object.assign({ opcode: 0x09 }, options)
));

const textFrame = Buffer.from('819461616161' + '61'.repeat(20), 'hex');
const pingFrame2 = Buffer.from('8900', 'hex');
const binaryFrame1 = createBinaryFrame(125);
const binaryFrame2 = createBinaryFrame(65535);
const binaryFrame3 = createBinaryFrame(200 * 1024);
const binaryFrame4 = createBinaryFrame(1024 * 1024);

const suite = new benchmark.Suite();
const receiver = new Receiver();

suite.add('ping frame (5 bytes payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(pingFrame1, deferred.resolve.bind(deferred));
  }
});
suite.add('ping frame (no payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(pingFrame2, deferred.resolve.bind(deferred));
  }
});
suite.add('text frame (20 bytes payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(textFrame, deferred.resolve.bind(deferred));
  }
});
suite.add('binary frame (125 bytes payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(binaryFrame1, deferred.resolve.bind(deferred));
  }
});
suite.add('binary frame (65535 bytes payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(binaryFrame2, deferred.resolve.bind(deferred));
  }
});
suite.add('binary frame (200 KiB payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(binaryFrame3, deferred.resolve.bind(deferred));
  }
});
suite.add('binary frame (1 MiB payload)', {
  defer: true,
  fn: (deferred) => {
    receiver.write(binaryFrame4, deferred.resolve.bind(deferred));
  }
});

suite.on('cycle', (e) => console.log(e.target.toString()));

if (require.main === module) {
  suite.run({ async: true });
} else {
  module.exports = suite;
}
<|endoftext|>


<commit_before>'use strict'

let emptyToNull = (data, outputData) => {
		for (let key in data) {
			let currentValue = null

			if (data[key] instanceof Array) {
				currentValue = emptyToNull(data[key], [])
			} else if (typeof data[key] === 'object') {
				currentValue = emptyToNull(data[key], {})
			} else if (data[key] === '') {
				currentValue = null
			} else {
				currentValue = data[key]
			}

			if (outputData instanceof Array) {
				outputData.push(currentValue)
				continue;
			}
			outputData[key] = currentValue
		}
		return outputData
	},
	getNested = ({parent, field}) => {
		if ((typeof parent !== 'object') || (typeof field !== 'string')) {
			return null
		}
		let fieldData = field.split('.'),
			currentElement = parent
		for (let i in fieldData) {
			let innerElement = fieldData[i]
			if (!currentElement || !currentElement[innerElement]) {
				return currentElement
			}
			currentElement = currentElement[innerElement]
		}
		return currentElement
	}

module.exports = {emptyToNull, getNested}
<commit_msg>fix for the toolbelt getNested<commit_after>'use strict'

let emptyToNull = (data, outputData) => {
		for (let key in data) {
			let currentValue = null

			if (data[key] instanceof Array) {
				currentValue = emptyToNull(data[key], [])
			} else if (typeof data[key] === 'object') {
				currentValue = emptyToNull(data[key], {})
			} else if (data[key] === '') {
				currentValue = null
			} else {
				currentValue = data[key]
			}

			if (outputData instanceof Array) {
				outputData.push(currentValue)
				continue;
			}
			outputData[key] = currentValue
		}
		return outputData
	},
	getNested = (parent, field) => {
		if ((typeof parent !== 'object') || (typeof field !== 'string')) {
			return null
		}
		let fieldData = field.split('.'),
			currentElement = parent
		for (let i in fieldData) {
			let innerElement = fieldData[i]
			if (!currentElement || !currentElement[innerElement]) {
				return currentElement
			}
			currentElement = currentElement[innerElement]
		}
		return currentElement
	}

module.exports = {emptyToNull, getNested}
<|endoftext|>


<commit_before>'use strict';

//  ---------------------------------------------------------------------------

const ccxt = require ('ccxt');
// const IncrementalOrderBook = require ('./base/IncrementalOrderBook');

//  ---------------------------------------------------------------------------

module.exports = class binance extends ccxt.binance {
    describe () {
        return this.deepExtend (super.describe (), {
            'has': {
                'fetchWsOrderBook': true,
                'fetchWsOHLCV': true,
                'fetchWsTrades': true,
            },
            'urls': {
                'api': {
                    // 'ws': 'wss://stream.binance.com:9443/ws',
                    // 'ws': 'wss://echo.websocket.org/',
                    'ws': 'ws://127.0.0.1:8080',
                },
            },
            'options': {
                'marketsByLowerCaseId': {},
            },
        });
    }

    async loadMarkets (reload = false, params = {}) {
        const markets = await super.loadMarkets (reload, params);
        let marketsByLowercaseId = this.safeValue (this.options, 'marketsByLowercaseId');
        if ((marketsByLowercaseId === undefined) || reload) {
            marketsByLowercaseId = {};
            for (let i = 0; i < this.symbols.length; i++) {
                const symbol = this.symbols[i];
                const lowercaseId = this.markets[symbol]['id'].toLowerCase ();
                this.markets[symbol]['lowercaseId'] = lowercaseId;
                marketsByLowercaseId[lowercaseId] = this.markets[symbol];
            }
            this.options['marketsByLowercaseId'] = marketsByLowercaseId;
        }
        return markets;
    }

    // getWsMessageHash (client, response) {
    //     return client.url;
    // }

    // async fetchWsTrades (symbol) {
    //     await this.loadMarkets ();
    //     const market = this.market (symbol);
    //     const url = this.urls['api']['ws'] + market['id'].toLowerCase () + '@trade';
    //     return await this.WsTradesMessage (url, url);
    // }

    // handleWsTrades (response) {
    //     const parsed = this.parseTrade (response);
    //     parsed['symbol'] = this.parseSymbol (response);
    //     return parsed;
    // }

    // async fetchWsOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
    //     await this.loadMarkets ();
    //     const interval = this.timeframes[timeframe];
    //     const market = this.market (symbol);
    //     const url = this.urls['api']['ws'] + market['id'].toLowerCase () + '@kline_' + interval;
    //     return await this.WsOHLCVMessage (url, url);
    // }

    // handleWsOHLCV (ohlcv) {
    //     const data = ohlcv['k'];
    //     const timestamp = this.safeInteger (data, 'T');
    //     const open = this.safeFloat (data, 'o');
    //     const high = this.safeFloat (data, 'h');
    //     const close = this.safeFloat (data, 'l');
    //     const low = this.safeFloat (data, 'c');
    //     const volume = this.safeFloat (data, 'v');
    //     return [timestamp, open, high, close, low, volume];
    // }

    async fetchWsOrderBook (symbol, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        // this should be executed much later
        // const orderbook = await this.fetchOrderBook (symbol, limit, params);
        // const request = {};
        const name = 'depth';
        const stream = market['lowercaseId'] + '@' + name;
        const url = this.urls['api']['ws'];// + '/' + stream;
        const requestId = this.nonce ();
        const request = {
            'method': 'SUBSCRIBE',
            'params': [
                stream,
            ],
            'id': requestId,
        };
        const messageHash = stream;
        const future = this.sendWsMessage (url, messageHash, this.extend (request, params), messageHash);
        const client = this.clients[url];
        client['futures'][requestId] = future;
        return future;
    }

    handleWsOrderBook (client, message) {
        //
        // initial snapshot is fetched with ccxt's fetchOrderBook
        // the feed does not include a snapshot, just the deltas
        //
        //     {
        //         "e": "depthUpdate", // Event type
        //         "E": 123456789, // Event time
        //         "s": "BNBBTC", // Symbol
        //         "U": 157, // First update ID in event
        //         "u": 160, // Final update ID in event
        //         "b": [ // bids
        //             [ "0.0024", "10" ], // price, size
        //         ],
        //         "a": [ // asks
        //             [ "0.0026", "100" ], // price, size
        //         ]
        //     }
        //
        const deltas = [];
        const nonce = message['u'];
        for (let i = 0; i < message['b'].length; i++) {
            const bid = message['b'][i];
            deltas.push ([nonce, 'absolute', 'bids', parseFloat (bid[0]), parseFloat (bid[1])]);
        }
        for (let i = 0; i < message['a'].length; i++) {
            const asks = message['a'][i];
            deltas.push ([nonce, 'absolute', 'asks', parseFloat (asks[0]), parseFloat (asks[1])]);
        }
        const symbol = this.parseSymbol (message);
        const incrementalBook = this.orderbooks[symbol];
        incrementalBook.update (deltas);
        const timestamp = this.safeInteger (message, 'E');
        incrementalBook.message['timestamp'] = timestamp;
        incrementalBook.message['datetime'] = this.iso8601 (timestamp);
        incrementalBook.message['nonce'] = message['u'];
        return incrementalBook.orderBook;
    }

    // parseSymbol (message) {
    //     return this.marketsById[message['s']]['symbol'];
    // }

    // handleWsDropped (client, message, messageHash) {
    //     const orderBookHash = 'wss://stream.binance.com:9443/ws/ethbtc@depth';
    //     if (messageHash !== undefined && messageHash.startsWith (orderBookHash)) {
    //         this.handleWsOrderBook (message);
    //     }
    // }

    signWsMessage (client, messageHash, message, params = {}) {
        // todo: binance signWsMessage not implemented yet
        return message;
    }

    // handleWsSubscriptionStatus (client, message) {
    //     //
    //     // todo: answer the question whether handleWsSubscriptionStatus should be renamed
    //     // and unified as handleWsResponse for any usage pattern that
    //     // involves an identified request/response sequence
    //     //
    //     //     {
    //     //         "result": null,
    //     //         "id": 1574649734450
    //     //     }
    //     //
    //     // --------------------------------------------------------------------
    //     //
    //     const channelId = this.safeString (message, 'chanId');
    //     this.options['subscriptionsByChannelId'][channelId] = message;
    //     return message;
    // }

    handleWsSubscriptionStatus (client, message) {
        //
        // todo: answer the question whether handleWsSubscriptionStatus should be renamed
        // and unified as handleWsResponse for any usage pattern that
        // involves an identified request/response sequence
        //
        //     {
        //         "result": null,
        //         "id": 1574649734450
        //     }
        //
        const channelId = this.safeString (message, 'channelID');
        this.options['subscriptionStatusByChannelId'][channelId] = message;
        const requestId = this.safeString (message, 'reqid');
        if (client.futures[requestId]) {
            delete client.futures[requestId];
        }
    }

    handleWsMessage (client, message) {
        const keys = Object.keys (client.futures);
        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];
            this.rejectWsFuture ()
        }
        // console.log (new Date (), JSON.stringify (message, null, 4));
        // console.log ('---------------------------------------------------------');
        // if (Array.isArray (message)) {
        //     const channelId = message[0].toString ();
        //     const subscriptionStatus = this.safeValue (this.options['subscriptionStatusByChannelId'], channelId, {});
        //     const subscription = this.safeValue (subscriptionStatus, 'subscription', {});
        //     const name = this.safeString (subscription, 'name');
        //     const methods = {
        //         'book': 'handleWsOrderBook',
        //         'ohlc': 'handleWsOHLCV',
        //         'ticker': 'handleWsTicker',
        //         'trade': 'handleWsTrades',
        //     };
        //     const method = this.safeString (methods, name);
        //     if (method === undefined) {
        //         return message;
        //     } else {
        //         return this[method] (client, message);
        //     }
        // } else {
        //     if (this.handleWsErrors (client, message)) {
        //         const event = this.safeString (message, 'event');
        //         const methods = {
        //             'heartbeat': 'handleWsHeartbeat',
        //             'systemStatus': 'handleWsSystemStatus',
        //             'subscriptionStatus': 'handleWsSubscriptionStatus',
        //         };
        //         const method = this.safeString (methods, event);
        //         if (method === undefined) {
        //             return message;
        //         } else {
        //             return this[method] (client, message);
        //         }
        //     }
        // }
    }
};

<commit_msg>binance cleanup<commit_after>'use strict';

//  ---------------------------------------------------------------------------

const ccxt = require ('ccxt');
const { NotImplemented } = require ('ccxt/js/base/errors');

//  ---------------------------------------------------------------------------

module.exports = class binance extends ccxt.binance {
    describe () {
        return this.deepExtend (super.describe (), {
            'has': {
                'fetchWsOrderBook': true,
                'fetchWsOHLCV': true,
                'fetchWsTrades': true,
            },
            'urls': {
                'api': {
                    // 'ws': 'wss://stream.binance.com:9443/ws',
                    // 'ws': 'wss://echo.websocket.org/',
                    'ws': 'ws://127.0.0.1:8080',
                },
            },
            'options': {
                'marketsByLowerCaseId': {},
            },
        });
    }

    async loadMarkets (reload = false, params = {}) {
        const markets = await super.loadMarkets (reload, params);
        let marketsByLowercaseId = this.safeValue (this.options, 'marketsByLowercaseId');
        if ((marketsByLowercaseId === undefined) || reload) {
            marketsByLowercaseId = {};
            for (let i = 0; i < this.symbols.length; i++) {
                const symbol = this.symbols[i];
                const lowercaseId = this.markets[symbol]['id'].toLowerCase ();
                this.markets[symbol]['lowercaseId'] = lowercaseId;
                marketsByLowercaseId[lowercaseId] = this.markets[symbol];
            }
            this.options['marketsByLowercaseId'] = marketsByLowercaseId;
        }
        return markets;
    }

    async fetchWsTrades (symbol) {
        //     await this.loadMarkets ();
        //     const market = this.market (symbol);
        //     const url = this.urls['api']['ws'] + market['id'].toLowerCase () + '@trade';
        //     return await this.WsTradesMessage (url, url);
        throw new NotImplemented (this.id + ' fetchWsTrades not implemented yet');
    }

    handleWsTrades (response) {
        //     const parsed = this.parseTrade (response);
        //     parsed['symbol'] = this.parseSymbol (response);
        //     return parsed;
        throw new NotImplemented (this.id + ' handleWsTrades not implemented yet');
    }

    async fetchWsOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
        //     await this.loadMarkets ();
        //     const interval = this.timeframes[timeframe];
        //     const market = this.market (symbol);
        //     const url = this.urls['api']['ws'] + market['id'].toLowerCase () + '@kline_' + interval;
        //     return await this.WsOHLCVMessage (url, url);
        throw new NotImplemented (this.id + ' fetchWsOHLCV not implemented yet');
    }

    handleWsOHLCV (ohlcv) {
        //     const data = ohlcv['k'];
        //     const timestamp = this.safeInteger (data, 'T');
        //     const open = this.safeFloat (data, 'o');
        //     const high = this.safeFloat (data, 'h');
        //     const close = this.safeFloat (data, 'l');
        //     const low = this.safeFloat (data, 'c');
        //     const volume = this.safeFloat (data, 'v');
        //     return [timestamp, open, high, close, low, volume];
        throw new NotImplemented (this.id + ' handleWsOHLCV not implemented yet ' + this.json (ohlcv));
    }

    async fetchWsOrderBook (symbol, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        // this should be executed much later
        // const orderbook = await this.fetchOrderBook (symbol, limit, params);
        // const request = {};
        const name = 'depth';
        const stream = market['lowercaseId'] + '@' + name;
        const url = this.urls['api']['ws']; // + '/' + stream;
        const requestId = this.nonce ();
        const request = {
            'method': 'SUBSCRIBE',
            'params': [
                stream,
            ],
            'id': requestId,
        };
        const messageHash = stream;
        const future = this.sendWsMessage (url, messageHash, this.extend (request, params), messageHash);
        const client = this.clients[url];
        client['futures'][requestId] = future;
        return future;
    }

    handleWsOrderBook (client, message) {
        //
        // initial snapshot is fetched with ccxt's fetchOrderBook
        // the feed does not include a snapshot, just the deltas
        //
        //     {
        //         "e": "depthUpdate", // Event type
        //         "E": 123456789, // Event time
        //         "s": "BNBBTC", // Symbol
        //         "U": 157, // First update ID in event
        //         "u": 160, // Final update ID in event
        //         "b": [ // bids
        //             [ "0.0024", "10" ], // price, size
        //         ],
        //         "a": [ // asks
        //             [ "0.0026", "100" ], // price, size
        //         ]
        //     }
        //
        const deltas = [];
        const nonce = message['u'];
        for (let i = 0; i < message['b'].length; i++) {
            const bid = message['b'][i];
            deltas.push ([nonce, 'absolute', 'bids', parseFloat (bid[0]), parseFloat (bid[1])]);
        }
        for (let i = 0; i < message['a'].length; i++) {
            const asks = message['a'][i];
            deltas.push ([nonce, 'absolute', 'asks', parseFloat (asks[0]), parseFloat (asks[1])]);
        }
        const symbol = this.parseSymbol (message);
        const incrementalBook = this.orderbooks[symbol];
        incrementalBook.update (deltas);
        const timestamp = this.safeInteger (message, 'E');
        incrementalBook.message['timestamp'] = timestamp;
        incrementalBook.message['datetime'] = this.iso8601 (timestamp);
        incrementalBook.message['nonce'] = message['u'];
        return incrementalBook.orderBook;
    }

    // parseSymbol (message) {
    //     return this.marketsById[message['s']]['symbol'];
    // }

    // handleWsDropped (client, message, messageHash) {
    //     const orderBookHash = 'wss://stream.binance.com:9443/ws/ethbtc@depth';
    //     if (messageHash !== undefined && messageHash.startsWith (orderBookHash)) {
    //         this.handleWsOrderBook (message);
    //     }
    // }

    signWsMessage (client, messageHash, message, params = {}) {
        // todo: binance signWsMessage not implemented yet
        return message;
    }

    // handleWsSubscriptionStatus (client, message) {
    //     //
    // todo: answer the question whether handleWsSubscriptionStatus should be renamed
    // and unified as handleWsResponse for any usage pattern that
    // involves an identified request/response sequence
    //     //
    //     //     {
    //     //         "result": null,
    //     //         "id": 1574649734450
    //     //     }
    //     //
    //     // --------------------------------------------------------------------
    //     //
    //     const channelId = this.safeString (message, 'chanId');
    //     this.options['subscriptionsByChannelId'][channelId] = message;
    //     return message;
    // }

    handleWsSubscriptionStatus (client, message) {
        //
        // todo: answer the question whether handleWsSubscriptionStatus should be renamed
        // and unified as handleWsResponse for any usage pattern that
        // involves an identified request/response sequence
        //
        //     {
        //         "result": null,
        //         "id": 1574649734450
        //     }
        //
        const channelId = this.safeString (message, 'channelID');
        this.options['subscriptionStatusByChannelId'][channelId] = message;
        const requestId = this.safeString (message, 'reqid');
        if (client.futures[requestId]) {
            delete client.futures[requestId];
        }
    }

    handleWsMessage (client, message) {
        console.log (message);
        //
        // const keys = Object.keys (client.futures);
        // for (let i = 0; i < keys.length; i++) {
        //     const key = keys[i];
        //     this.rejectWsFuture ()
        // }
        //
        //---------------------------------------------------------------------
        //
        // console.log (new Date (), JSON.stringify (message, null, 4));
        // console.log ('---------------------------------------------------------');
        // if (Array.isArray (message)) {
        //     const channelId = message[0].toString ();
        //     const subscriptionStatus = this.safeValue (this.options['subscriptionStatusByChannelId'], channelId, {});
        //     const subscription = this.safeValue (subscriptionStatus, 'subscription', {});
        //     const name = this.safeString (subscription, 'name');
        //     const methods = {
        //         'book': 'handleWsOrderBook',
        //         'ohlc': 'handleWsOHLCV',
        //         'ticker': 'handleWsTicker',
        //         'trade': 'handleWsTrades',
        //     };
        //     const method = this.safeString (methods, name);
        //     if (method === undefined) {
        //         return message;
        //     } else {
        //         return this[method] (client, message);
        //     }
        // } else {
        //     if (this.handleWsErrors (client, message)) {
        //         const event = this.safeString (message, 'event');
        //         const methods = {
        //             'heartbeat': 'handleWsHeartbeat',
        //             'systemStatus': 'handleWsSystemStatus',
        //             'subscriptionStatus': 'handleWsSubscriptionStatus',
        //         };
        //         const method = this.safeString (methods, event);
        //         if (method === undefined) {
        //             return message;
        //         } else {
        //             return this[method] (client, message);
        //         }
        //     }
        // }
    }
};

<|endoftext|>


<commit_before>const log = require('../util/log');

class Video {
    constructor (runtime) {
        /**
         * Reference to the owning Runtime.
         * @type{!Runtime}
         */
        this.runtime = runtime;

        /**
         * Default value for mirrored frames.
         * @type boolean
         */
        this.mirror = true;

        /**
         * Cache frames for this many ms.
         * @type number
         */
        this._frameCacheTimeout = 16;

        /**
         * DOM Video element
         * @private
         */
        this._video = null;

        /**
         * Usermedia stream track
         * @private
         */
        this._track = null;

        /**
         * Stores some canvas/frame data per resolution/mirror states
         */
        this._workspace = [];

        /**
         * Id representing a Scratch Renderer skin the video is rendered to for
         * previewing.
         * @type {number}
         */
        this._skinId = -1;

        /**
         * The Scratch Renderer Skin object.
         * @type {Skin}
         */
        this._skin = null;

        /**
         * Id for a drawable using the video's skin that will render as a video
         * preview.
         * @type {Drawable}
         */
        this._drawable = -1;

        /**
         * Store the last state of the video transparency ghost effect
         * @type {number}
         */
        this._ghost = 0;
    }

    static get FORMAT_IMAGE_DATA () {
        return 'image-data';
    }

    static get FORMAT_CANVAS () {
        return 'canvas';
    }

    /**
     * Dimensions the video stream is analyzed at after its rendered to the
     * sample canvas.
     * @type {Array.<number>}
     */
    static get DIMENSIONS () {
        return [480, 360];
    }

    /**
     * Order preview drawable is inserted at in the renderer.
     * @type {number}
     */
    static get ORDER () {
        return 1;
    }

    /**
     * Request video be enabled.  Sets up video, creates video skin and enables preview.
     *
     * ioDevices.video.requestVideo()
     *
     * @return {Promise.<Video>} resolves a promise to this IO device when video is ready.
     */
    enableVideo () {
        this.enabled = true;
        return this._setupVideo();
    }

    /**
     * Disable video stream (turn video off)
     */
    disableVideo () {
        this.enabled = false;
        // If we have begun a setup process, call _teardown after it completes
        if (this._singleSetup) {
            this._singleSetup
                .then(this._teardown.bind(this))
                .catch(err => this.onError(err));
        }
    }

    /**
     * async part of disableVideo
     * @private
     */
    _teardown () {
        // we might be asked to re-enable before _teardown is called, just ignore it.
        if (this.enabled === false) {
            this._disablePreview();
            this._singleSetup = null;
            // by clearing refs to video and track, we should lose our hold over the camera
            this._video = null;
            if (this._track) {
                this._track.stop();
            }
            this._track = null;
        }
    }

    /**
     * Return frame data from the video feed in a specified dimensions, format, and mirroring.
     *
     * @param {object} frameInfo A descriptor of the frame you would like to receive.
     * @param {Array.<number>} frameInfo.dimensions [width, height] array of numbers.  Defaults to [480,360]
     * @param {boolean} frameInfo.mirror If you specificly want a mirror/non-mirror frame, defaults to the global
     *                                   mirror state (ioDevices.video.mirror)
     * @param {string} frameInfo.format Requested video format, available formats are 'image-data' and 'canvas'.
     * @param {number} frameInfo.cacheTimeout Will reuse previous image data if the time since capture is less than
     *                                        the cacheTimeout.  Defaults to 16ms.
     *
     * @return {ArrayBuffer|Canvas|string|null} Frame data in requested format, null when errors.
     */
    getFrame ({
        dimensions = Video.DIMENSIONS,
        mirror = this.mirror,
        format = Video.FORMAT_IMAGE_DATA,
        cacheTimeout = this._frameCacheTimeout
    }) {
        if (!this.videoReady) {
            return null;
        }
        const [width, height] = dimensions;
        const workspace = this._getWorkspace({dimensions, mirror: Boolean(mirror)});
        const {videoWidth, videoHeight} = this._video;
        const {canvas, context, lastUpdate, cacheData} = workspace;
        const now = Date.now();

        // if the canvas hasn't been updated...
        if (lastUpdate + cacheTimeout < now) {

            if (mirror) {
                context.scale(-1, 1);
                context.translate(width * -1, 0);
            }

            context.drawImage(this._video,
                // source x, y, width, height
                0, 0, videoWidth, videoHeight,
                // dest x, y, width, height
                0, 0, width, height
            );

            context.resetTransform();
            workspace.lastUpdate = now;
        }

        // each data type has it's own data cache, but the canvas is the same
        if (!cacheData[format]) {
            cacheData[format] = {lastUpdate: 0};
        }
        const formatCache = cacheData[format];

        if (formatCache.lastUpdate + cacheTimeout < now) {
            if (format === Video.FORMAT_IMAGE_DATA) {
                formatCache.lastData = context.getImageData(0, 0, width, height);
            } else if (format === Video.FORMAT_CANVAS) {
                // this will never change
                formatCache.lastUpdate = Infinity;
                formatCache.lastData = canvas;
            } else {
                log.error(`video io error - unimplemented format ${format}`);
                // cache the null result forever, don't log about it again..
                formatCache.lastUpdate = Infinity;
                formatCache.lastData = null;
            }

            // rather than set to now, this data is as stale as it's canvas is
            formatCache.lastUpdate = Math.max(workspace.lastUpdate, formatCache.lastUpdate);
        }

        return formatCache.lastData;
    }

    /**
     * Set the preview ghost effect
     * @param {number} ghost from 0 (visible) to 100 (invisible) - ghost effect
     */
    setPreviewGhost (ghost) {
        this._ghost = ghost;
        if (this._drawable) {
            this.runtime.renderer.updateDrawableProperties(this._drawable, {ghost});
        }
    }

    /**
     * Method called when an error happens.  Default implementation is just to log error.
     *
     * @abstract
     * @param {Error} error An error object from getUserMedia or other source of error.
     */
    onError (error) {
        log.error('Unhandled video io device error', error);
    }

    /**
     * Create a video stream.
     * Should probably be moved to -render or somewhere similar later
     * @private
     * @return {Promise} When video has been received, rejected if video is not received
     */
    _setupVideo () {
        // We cache the result of this setup so that we can only ever have a single
        // video/getUserMedia request happen at a time.
        if (this._singleSetup) {
            return this._singleSetup;
        }

        this._singleSetup = new Promise((resolve, reject) => {
            navigator.getUserMedia({
                audio: false,
                video: {
                    width: {min: 480, ideal: 640},
                    height: {min: 360, ideal: 480}
                }
            }, resolve, reject);
        })
            .then(stream => {
                this._video = document.createElement('video');
                this._video.src = window.URL.createObjectURL(stream);
                // Hint to the stream that it should load. A standard way to do this
                // is add the video tag to the DOM. Since this extension wants to
                // hide the video tag and instead render a sample of the stream into
                // the webgl rendered Scratch canvas, another hint like this one is
                // needed.
                this._track = stream.getTracks()[0];
                this._setupPreview();
                return this;
            })
            .catch(error => {
                this._singleSetup = null;
                this.onError(error);
            });

        return this._singleSetup;
    }

    _disablePreview () {
        if (this._skin) {
            this._skin.clear();
            this.runtime.renderer.updateDrawableProperties(this._drawable, {visible: false});
        }
        this._renderPreviewFrame = null;
    }

    _setupPreview () {
        const {renderer} = this.runtime;
        if (!renderer) return;

        if (this._skinId === -1 && this._skin === null && this._drawable === -1) {
            this._skinId = renderer.createPenSkin();
            this._skin = renderer._allSkins[this._skinId];
            this._drawable = renderer.createDrawable();
            renderer.setDrawableOrder(
                this._drawable,
                Video.ORDER
            );
            renderer.updateDrawableProperties(this._drawable, {
                skinId: this._skinId
            });
        }

        // if we haven't already created and started a preview frame render loop, do so
        if (!this._renderPreviewFrame) {
            renderer.updateDrawableProperties(this._drawable, {
                ghost: this._ghost,
                visible: true
            });

            this._renderPreviewFrame = () => {
                clearTimeout(this._renderPreviewTimeout);
                if (!this._renderPreviewFrame) {
                    return;
                }

                this._renderPreviewTimeout = setTimeout(this._renderPreviewFrame, this.runtime.currentStepTime);

                const canvas = this.getFrame({format: Video.FORMAT_CANVAS});

                if (!canvas) {
                    this._skin.clear();
                    return;
                }

                const xOffset = Video.DIMENSIONS[0] / -2;
                const yOffset = Video.DIMENSIONS[1] / 2;
                this._skin.drawStamp(canvas, xOffset, yOffset);
                this.runtime.requestRedraw();
            };

            this._renderPreviewFrame();
        }
    }

    get videoReady () {
        if (!this.enabled) {
            return false;
        }
        if (!this._video) {
            return false;
        }
        if (!this._track) {
            return false;
        }
        const {videoWidth, videoHeight} = this._video;
        if (typeof videoWidth !== 'number' || typeof videoHeight !== 'number') {
            return false;
        }
        if (videoWidth === 0 || videoHeight === 0) {
            return false;
        }
        return true;
    }

    /**
     * get an internal workspace for canvas/context/caches
     * this uses some document stuff to create a canvas and what not, probably needs abstraction
     * into the renderer layer?
     * @private
     * @return {object} A workspace for canvas/data storage.  Internal format not documented intentionally
     */
    _getWorkspace ({dimensions, mirror}) {
        let workspace = this._workspace.find(space => (
            space.dimensions.join('-') === dimensions.join('-') &&
            space.mirror === mirror
        ));
        if (!workspace) {
            workspace = {
                dimensions,
                mirror,
                canvas: document.createElement('canvas'),
                lastUpdate: 0,
                cacheData: {}
            };
            workspace.canvas.width = dimensions[0];
            workspace.canvas.height = dimensions[1];
            workspace.context = workspace.canvas.getContext('2d');
            this._workspace.push(workspace);
        }
        return workspace;
    }

}

module.exports = Video;
<commit_msg>Use new mediaDevices API for getUserMedia<commit_after>const log = require('../util/log');

class Video {
    constructor (runtime) {
        /**
         * Reference to the owning Runtime.
         * @type{!Runtime}
         */
        this.runtime = runtime;

        /**
         * Default value for mirrored frames.
         * @type boolean
         */
        this.mirror = true;

        /**
         * Cache frames for this many ms.
         * @type number
         */
        this._frameCacheTimeout = 16;

        /**
         * DOM Video element
         * @private
         */
        this._video = null;

        /**
         * Usermedia stream track
         * @private
         */
        this._track = null;

        /**
         * Stores some canvas/frame data per resolution/mirror states
         */
        this._workspace = [];

        /**
         * Id representing a Scratch Renderer skin the video is rendered to for
         * previewing.
         * @type {number}
         */
        this._skinId = -1;

        /**
         * The Scratch Renderer Skin object.
         * @type {Skin}
         */
        this._skin = null;

        /**
         * Id for a drawable using the video's skin that will render as a video
         * preview.
         * @type {Drawable}
         */
        this._drawable = -1;

        /**
         * Store the last state of the video transparency ghost effect
         * @type {number}
         */
        this._ghost = 0;
    }

    static get FORMAT_IMAGE_DATA () {
        return 'image-data';
    }

    static get FORMAT_CANVAS () {
        return 'canvas';
    }

    /**
     * Dimensions the video stream is analyzed at after its rendered to the
     * sample canvas.
     * @type {Array.<number>}
     */
    static get DIMENSIONS () {
        return [480, 360];
    }

    /**
     * Order preview drawable is inserted at in the renderer.
     * @type {number}
     */
    static get ORDER () {
        return 1;
    }

    /**
     * Request video be enabled.  Sets up video, creates video skin and enables preview.
     *
     * ioDevices.video.requestVideo()
     *
     * @return {Promise.<Video>} resolves a promise to this IO device when video is ready.
     */
    enableVideo () {
        this.enabled = true;
        return this._setupVideo();
    }

    /**
     * Disable video stream (turn video off)
     */
    disableVideo () {
        this.enabled = false;
        // If we have begun a setup process, call _teardown after it completes
        if (this._singleSetup) {
            this._singleSetup
                .then(this._teardown.bind(this))
                .catch(err => this.onError(err));
        }
    }

    /**
     * async part of disableVideo
     * @private
     */
    _teardown () {
        // we might be asked to re-enable before _teardown is called, just ignore it.
        if (this.enabled === false) {
            this._disablePreview();
            this._singleSetup = null;
            // by clearing refs to video and track, we should lose our hold over the camera
            this._video = null;
            if (this._track) {
                this._track.stop();
            }
            this._track = null;
        }
    }

    /**
     * Return frame data from the video feed in a specified dimensions, format, and mirroring.
     *
     * @param {object} frameInfo A descriptor of the frame you would like to receive.
     * @param {Array.<number>} frameInfo.dimensions [width, height] array of numbers.  Defaults to [480,360]
     * @param {boolean} frameInfo.mirror If you specificly want a mirror/non-mirror frame, defaults to the global
     *                                   mirror state (ioDevices.video.mirror)
     * @param {string} frameInfo.format Requested video format, available formats are 'image-data' and 'canvas'.
     * @param {number} frameInfo.cacheTimeout Will reuse previous image data if the time since capture is less than
     *                                        the cacheTimeout.  Defaults to 16ms.
     *
     * @return {ArrayBuffer|Canvas|string|null} Frame data in requested format, null when errors.
     */
    getFrame ({
        dimensions = Video.DIMENSIONS,
        mirror = this.mirror,
        format = Video.FORMAT_IMAGE_DATA,
        cacheTimeout = this._frameCacheTimeout
    }) {
        if (!this.videoReady) {
            return null;
        }
        const [width, height] = dimensions;
        const workspace = this._getWorkspace({dimensions, mirror: Boolean(mirror)});
        const {videoWidth, videoHeight} = this._video;
        const {canvas, context, lastUpdate, cacheData} = workspace;
        const now = Date.now();

        // if the canvas hasn't been updated...
        if (lastUpdate + cacheTimeout < now) {

            if (mirror) {
                context.scale(-1, 1);
                context.translate(width * -1, 0);
            }

            context.drawImage(this._video,
                // source x, y, width, height
                0, 0, videoWidth, videoHeight,
                // dest x, y, width, height
                0, 0, width, height
            );

            context.resetTransform();
            workspace.lastUpdate = now;
        }

        // each data type has it's own data cache, but the canvas is the same
        if (!cacheData[format]) {
            cacheData[format] = {lastUpdate: 0};
        }
        const formatCache = cacheData[format];

        if (formatCache.lastUpdate + cacheTimeout < now) {
            if (format === Video.FORMAT_IMAGE_DATA) {
                formatCache.lastData = context.getImageData(0, 0, width, height);
            } else if (format === Video.FORMAT_CANVAS) {
                // this will never change
                formatCache.lastUpdate = Infinity;
                formatCache.lastData = canvas;
            } else {
                log.error(`video io error - unimplemented format ${format}`);
                // cache the null result forever, don't log about it again..
                formatCache.lastUpdate = Infinity;
                formatCache.lastData = null;
            }

            // rather than set to now, this data is as stale as it's canvas is
            formatCache.lastUpdate = Math.max(workspace.lastUpdate, formatCache.lastUpdate);
        }

        return formatCache.lastData;
    }

    /**
     * Set the preview ghost effect
     * @param {number} ghost from 0 (visible) to 100 (invisible) - ghost effect
     */
    setPreviewGhost (ghost) {
        this._ghost = ghost;
        if (this._drawable) {
            this.runtime.renderer.updateDrawableProperties(this._drawable, {ghost});
        }
    }

    /**
     * Method called when an error happens.  Default implementation is just to log error.
     *
     * @abstract
     * @param {Error} error An error object from getUserMedia or other source of error.
     */
    onError (error) {
        log.error('Unhandled video io device error', error);
    }

    /**
     * Create a video stream.
     * Should probably be moved to -render or somewhere similar later
     * @private
     * @return {Promise} When video has been received, rejected if video is not received
     */
    _setupVideo () {
        // We cache the result of this setup so that we can only ever have a single
        // video/getUserMedia request happen at a time.
        if (this._singleSetup) {
            return this._singleSetup;
        }

        this._singleSetup = navigator.mediaDevices.getUserMedia({
            audio: false,
            video: {
                width: {min: 480, ideal: 640},
                height: {min: 360, ideal: 480}
            }
        })
            .then(stream => {
                this._video = document.createElement('video');
                // Use the new srcObject API, falling back to createObjectURL
                try {
                    this._video.srcObject = stream;
                } catch (error) {
                    this._video.src = window.URL.createObjectURL(stream);
                }
                this._video.play();
                // Hint to the stream that it should load. A standard way to do this
                // is add the video tag to the DOM. Since this extension wants to
                // hide the video tag and instead render a sample of the stream into
                // the webgl rendered Scratch canvas, another hint like this one is
                // needed.
                this._video.play(); // Needed for Safari/Firefox, Chrome auto-plays.
                this._track = stream.getTracks()[0];
                this._setupPreview();
                return this;
            })
            .catch(error => {
                this._singleSetup = null;
                this.onError(error);
            });

        return this._singleSetup;
    }

    _disablePreview () {
        if (this._skin) {
            this._skin.clear();
            this.runtime.renderer.updateDrawableProperties(this._drawable, {visible: false});
        }
        this._renderPreviewFrame = null;
    }

    _setupPreview () {
        const {renderer} = this.runtime;
        if (!renderer) return;

        if (this._skinId === -1 && this._skin === null && this._drawable === -1) {
            this._skinId = renderer.createPenSkin();
            this._skin = renderer._allSkins[this._skinId];
            this._drawable = renderer.createDrawable();
            renderer.setDrawableOrder(
                this._drawable,
                Video.ORDER
            );
            renderer.updateDrawableProperties(this._drawable, {
                skinId: this._skinId
            });
        }

        // if we haven't already created and started a preview frame render loop, do so
        if (!this._renderPreviewFrame) {
            renderer.updateDrawableProperties(this._drawable, {
                ghost: this._ghost,
                visible: true
            });

            this._renderPreviewFrame = () => {
                clearTimeout(this._renderPreviewTimeout);
                if (!this._renderPreviewFrame) {
                    return;
                }

                this._renderPreviewTimeout = setTimeout(this._renderPreviewFrame, this.runtime.currentStepTime);

                const canvas = this.getFrame({format: Video.FORMAT_CANVAS});

                if (!canvas) {
                    this._skin.clear();
                    return;
                }

                const xOffset = Video.DIMENSIONS[0] / -2;
                const yOffset = Video.DIMENSIONS[1] / 2;
                this._skin.drawStamp(canvas, xOffset, yOffset);
                this.runtime.requestRedraw();
            };

            this._renderPreviewFrame();
        }
    }

    get videoReady () {
        if (!this.enabled) {
            return false;
        }
        if (!this._video) {
            return false;
        }
        if (!this._track) {
            return false;
        }
        const {videoWidth, videoHeight} = this._video;
        if (typeof videoWidth !== 'number' || typeof videoHeight !== 'number') {
            return false;
        }
        if (videoWidth === 0 || videoHeight === 0) {
            return false;
        }
        return true;
    }

    /**
     * get an internal workspace for canvas/context/caches
     * this uses some document stuff to create a canvas and what not, probably needs abstraction
     * into the renderer layer?
     * @private
     * @return {object} A workspace for canvas/data storage.  Internal format not documented intentionally
     */
    _getWorkspace ({dimensions, mirror}) {
        let workspace = this._workspace.find(space => (
            space.dimensions.join('-') === dimensions.join('-') &&
            space.mirror === mirror
        ));
        if (!workspace) {
            workspace = {
                dimensions,
                mirror,
                canvas: document.createElement('canvas'),
                lastUpdate: 0,
                cacheData: {}
            };
            workspace.canvas.width = dimensions[0];
            workspace.canvas.height = dimensions[1];
            workspace.context = workspace.canvas.getContext('2d');
            this._workspace.push(workspace);
        }
        return workspace;
    }

}

module.exports = Video;
<|endoftext|>