<commit_before>const request = require('request');
const config = require('./config');
const db = require('./db');
const {promisify} = require('util');
const reqAsync = promisify(request);
const path = require('path');
const debug = require('./debug');
const REGEX_RAW_GIST_URL = /^https?:\/\/gist\.githubusercontent\.com\/(.+?\/[0-9a-f]+\/raw\/(?:[0-9a-f]+\/)?.+\..+)$/i;
const CDN_URL = 'https://cdn.rawgit.com';
const GIT_API_URL = 'https://api.github.com/';
const REGISTRY_URL = 'https://registry.npmjs.org/';
const GEO_IP = 'http://freegeoip.net/json/';

const pkgApi = async (repo, pkg) => {
  try {
    const res = await reqAsync(`https://wzrd.in/standalone/${repo}`);

    if (res.statusCode === 200 && res.body) {
      const cdnlink = await createGist(pkg, res.body);
      return cdnlink;
    }
    return false;
  } catch (e) {
    return false;
  }
};
const validatePackage = async (pkg, ver) => {
  try {
    ver = !ver ? 'latest' : ver;
    const res = await reqAsync(`${REGISTRY_URL + pkg}/${ver}`);
    const body = JSON.parse(res.body);

    if (res.statusCode === 200 && body) {
      return {name: body.name, version: body.version};
    }
    return false;
  } catch (e) {
    return false;
  }
};
const createGist = async (pkg, content) => {
  try {
    const data = {
      'description': `Umdfied build of ${pkg}`,
      'public': true,
      'files': {}
    };
    const fname = `${pkg}.min.js`;
    data.files[fname] = {content};

    const reqOpts = {
      method: 'POST',
      url: '/gists',
      headers: {
        'Authorization': `token ${config.GIT_TOKEN}`,
        'User-Agent': 'Umdfied-App'
      },
      baseUrl: GIT_API_URL,
      json: true,
      body: data
    };
    const res = await reqAsync(reqOpts);

    const body = res.body;
    debug(body, 'createGist');
    let cdnurl = body.files[fname].raw_url.replace(REGEX_RAW_GIST_URL, `${CDN_URL}/$1`);
    return cdnurl;
  } catch (e) {
    console.log(e);
  }
};
const getCountry = async ip => {
  try {
    const res = await reqAsync({url: GEO_IP + ip, json: true});

    const country = res.body.country_name;
    if (country) {
      return country;
    }
    return 'anonymous';
  } catch (e) {
    return 'anonymous';
  }
};
const normalizeIp = ip => ip.replace(/^::ffff:/i, '');
exports.umdfied = async (pkg, ver, ip) => {
  try {
    ip = normalizeIp(ip);
    const usrCountry = await getCountry(ip);
    debug(usrCountry, 'usrCountry');
    await db.saveUsrInfo({ip, country: usrCountry});
    debug('Checking DB', 'db');
    const repoInfo = await validatePackage(pkg, ver);

    if (!repoInfo) {
      return false;
    }
    const fromDb = await db.getPkg(repoInfo.name, repoInfo.version);
    if (fromDb) {
      return {gitCdn: fromDb.cdn, semver: fromDb.version};
    }
    const repo = `${repoInfo.name}@${repoInfo.version}`;
    debug(repo, 'repo');
    const gitCdn = await pkgApi(repo, pkg);
    debug(gitCdn, 'gitCdn');
    if (!gitCdn) {
      return false;
    }
    await db.savePkg(repoInfo.name, repoInfo.version, gitCdn);
    return {gitCdn, semver: repoInfo.version};
  } catch (e) {
    debug(`${e}\n${e.stack}`, 'umdfied Error');
    return false;
  }
};
<commit_msg>change wzrd.in to own fork<commit_after>const request = require('request');
const config = require('./config');
const db = require('./db');
const {promisify} = require('util');
const reqAsync = promisify(request);
const path = require('path');
const debug = require('./debug');
const REGEX_RAW_GIST_URL = /^https?:\/\/gist\.githubusercontent\.com\/(.+?\/[0-9a-f]+\/raw\/(?:[0-9a-f]+\/)?.+\..+)$/i;
const CDN_URL = 'https://cdn.rawgit.com';
const GIT_API_URL = 'https://api.github.com/';
const REGISTRY_URL = 'https://registry.npmjs.org/';
const GEO_IP = 'http://freegeoip.net/json/';

const pkgApi = async (repo, pkg) => {
  try {
    const res = await reqAsync(`https://umdfied-cdn.herokuapp.com/standalone/${repo}`);

    if (res.statusCode === 200 && res.body) {
      const cdnlink = await createGist(pkg, res.body);
      return cdnlink;
    }
    return false;
  } catch (e) {
    return false;
  }
};
const validatePackage = async (pkg, ver) => {
  try {
    ver = !ver ? 'latest' : ver;
    const res = await reqAsync(`${REGISTRY_URL + pkg}/${ver}`);
    const body = JSON.parse(res.body);

    if (res.statusCode === 200 && body) {
      return {name: body.name, version: body.version};
    }
    return false;
  } catch (e) {
    return false;
  }
};
const createGist = async (pkg, content) => {
  try {
    const data = {
      'description': `Umdfied build of ${pkg}`,
      'public': true,
      'files': {}
    };
    const fname = `${pkg}.min.js`;
    data.files[fname] = {content};

    const reqOpts = {
      method: 'POST',
      url: '/gists',
      headers: {
        'Authorization': `token ${config.GIT_TOKEN}`,
        'User-Agent': 'Umdfied-App'
      },
      baseUrl: GIT_API_URL,
      json: true,
      body: data
    };
    const res = await reqAsync(reqOpts);

    const body = res.body;
    debug(body, 'createGist');
    let cdnurl = body.files[fname].raw_url.replace(REGEX_RAW_GIST_URL, `${CDN_URL}/$1`);
    return cdnurl;
  } catch (e) {
    console.log(e);
  }
};
const getCountry = async ip => {
  try {
    const res = await reqAsync({url: GEO_IP + ip, json: true});

    const country = res.body.country_name;
    if (country) {
      return country;
    }
    return 'anonymous';
  } catch (e) {
    return 'anonymous';
  }
};
const normalizeIp = ip => ip.replace(/^::ffff:/i, '');
exports.umdfied = async (pkg, ver, ip) => {
  try {
    ip = normalizeIp(ip);
    const usrCountry = await getCountry(ip);
    debug(usrCountry, 'usrCountry');
    await db.saveUsrInfo({ip, country: usrCountry});
    debug('Checking DB', 'db');
    const repoInfo = await validatePackage(pkg, ver);

    if (!repoInfo) {
      return false;
    }
    const fromDb = await db.getPkg(repoInfo.name, repoInfo.version);
    if (fromDb) {
      return {gitCdn: fromDb.cdn, semver: fromDb.version};
    }
    const repo = `${repoInfo.name}@${repoInfo.version}`;
    debug(repo, 'repo');
    const gitCdn = await pkgApi(repo, pkg);
    debug(gitCdn, 'gitCdn');
    if (!gitCdn) {
      return false;
    }
    await db.savePkg(repoInfo.name, repoInfo.version, gitCdn);
    return {gitCdn, semver: repoInfo.version};
  } catch (e) {
    debug(`${e}\n${e.stack}`, 'umdfied Error');
    return false;
  }
};
<|endoftext|>


<commit_before>/**
 * @file Thead component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

import Th from '../_Th';

import HorizontalAlign from '../_statics/HorizontalAlign';
import SelectMode from '../_statics/SelectMode';
import SelectAllMode from '../_statics/SelectAllMode';
import SortingType from '../_statics/SortingType';

import Util from '../_vendors/Util';
import TableFragment from '../_statics/TableFragment';
import TableCalculation from '../_vendors/TableCalculation';

class Thead extends Component {

    static Align = HorizontalAlign;
    static SelectMode = SelectMode;
    static SelectAllMode = SelectAllMode;
    static SortingType = SortingType;

    constructor(props, ...restArgs) {
        super(props, ...restArgs);
    }

    render() {

        const {
            className, style, columns, baseColIndex, data,
            sorting, defaultSortingType, sortingAscIconCls, sortingDescIconCls, onSortChange,
            ...restProps
        } = this.props;

        return (

                {
                    columns && columns.map((row, rowIndex) => row ?

                            {
                                TableCalculation.getColumnsWithSpan(TableFragment.HEAD, row, data)
                                                .map(({column, span}, colIndex) => column ?
                                                    <Th {...restProps}
                                                        key={colIndex}
                                                        className={column.headClassName}
                                                        style={column.headStyle}
                                                        data={data}
                                                        renderer={column.headRenderer}
                                                        align={column.headAlign || column.align}
                                                        colIndex={baseColIndex + colIndex}
                                                        rowSpan={column.rowSpan}
                                                        colSpan={column.colSpan}
                                                        sorting={sorting}
                                                        defaultSortingType={column.defaultSortingType || defaultSortingType}
                                                        sortingAscIconCls={sortingAscIconCls}
                                                        sortingDescIconCls={sortingDescIconCls}
                                                        sortable={column.sortable}
                                                        sortingProp={column.sortingProp}
                                                        onSortChange={onSortChange}/>
                                                    :
                                                    null
                                                )
                            }

                        :
                        null
                    )
                }

        );

    }
}

Thead.propTypes = {

    className: PropTypes.string,
    style: PropTypes.object,

    /**
     * Children passed into table header.
     */
    columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({

        /**
         * fixed position of column ( true / 'left' / 'right' )
         */
        fixed: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(Util.enumerateValue(HorizontalAlign))]),

        /**
         * width of column
         */
        width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

        /**
         * minimum width of column
         */
        minWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

        /**
         * align of current column
         */
        align: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The class name of header.
         */
        headClassName: PropTypes.string,

        /**
         * Override the styles of header.
         */
        headStyle: PropTypes.object,

        /**
         * align of table header cell
         */
        headAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table head.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        headRenderer: PropTypes.any,

        /**
         * column span of table header
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        headSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * The class name of td.
         */
        bodyClassName: PropTypes.string,

        /**
         * Override the styles of td.
         */
        bodyStyle: PropTypes.object,

        /**
         * align of table body cell
         */
        bodyAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table body.
         *  (1) callback:
         *      function (rowData, rowIndex, colIndex) {
         *          return rowData.id;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        bodyRenderer: PropTypes.any,

        /**
         * column span of table body
         *  (1) function callback:
         *      function (rowData, colIndex, rowIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        bodySpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * The class name of footer.
         */
        footClassName: PropTypes.string,

        /**
         * Override the styles of footer.
         */
        footStyle: PropTypes.object,

        /**
         * align of table footer cell
         */
        footAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table foot.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        footRenderer: PropTypes.any,

        /**
         * column span of table foot.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        footSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * If true,this column can be sorted.
         */
        sortable: PropTypes.bool,

        /**
         * Sorting property.
         */
        sortingProp: PropTypes.string,

        defaultSortingType: PropTypes.oneOf(Util.enumerateValue(SortingType))

    }))).isRequired,

    data: PropTypes.array,
    baseColIndex: PropTypes.number,

    /**
     * sorting
     */
    sorting: PropTypes.shape({
        prop: PropTypes.string,
        type: PropTypes.oneOf(Util.enumerateValue(SortingType))
    }),
    defaultSortingType: PropTypes.oneOf(Util.enumerateValue(SortingType)),
    sortingAscIconCls: PropTypes.string,
    sortingDescIconCls: PropTypes.string,

    onSortChange: PropTypes.func

};

Thead.defaultProps = {

    baseColIndex: 0,

    defaultSortingType: SortingType.ASC,
    sortingAscIconCls: 'fas fa-sort-up',
    sortingDescIconCls: 'fas fa-sort-down'

};

export default Thead;
<commit_msg>update Thead<commit_after>/**
 * @file Thead component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

import Th from '../_Th';

import HorizontalAlign from '../_statics/HorizontalAlign';
import SelectMode from '../_statics/SelectMode';
import SelectAllMode from '../_statics/SelectAllMode';
import SortingType from '../_statics/SortingType';

import Util from '../_vendors/Util';
import TableFragment from '../_statics/TableFragment';
import TableCalculation from '../_vendors/TableCalculation';

class Thead extends Component {

    static Align = HorizontalAlign;
    static SelectMode = SelectMode;
    static SelectAllMode = SelectAllMode;
    static SortingType = SortingType;

    constructor(props, ...restArgs) {
        super(props, ...restArgs);
    }

    render() {

        const {
            className, style, columns, baseColIndex, data,
            sorting, defaultSortingType, sortingAscIconCls, sortingDescIconCls, onSortChange,
            ...restProps
        } = this.props;

        return (

                {
                    columns && columns.map((row, rowIndex) => row ?

                            {
                                TableCalculation.getColumnsWithSpan(TableFragment.HEAD, row, data).map(({column, span}, colIndex) => column ?
                                    <Th {...restProps}
                                        key={colIndex}
                                        className={column.headClassName}
                                        style={column.headStyle}
                                        data={data}
                                        renderer={column.headRenderer}
                                        align={column.headAlign || column.align}
                                        colIndex={baseColIndex + colIndex}
                                        rowSpan={column.rowSpan}
                                        colSpan={column.colSpan}
                                        noWrap={TableCalculation.handleNoWrap(column.headNoWrap, column.noWrap, {
                                            data,
                                            rowIndex,
                                            colIndex: baseColIndex + colIndex
                                        })}
                                        sorting={sorting}
                                        defaultSortingType={column.defaultSortingType || defaultSortingType}
                                        sortingAscIconCls={sortingAscIconCls}
                                        sortingDescIconCls={sortingDescIconCls}
                                        sortable={column.sortable}
                                        sortingProp={column.sortingProp}
                                        onSortChange={onSortChange}/>
                                    :
                                    null
                                )
                            }

                        :
                        null
                    )
                }

        );

    }
}

Thead.propTypes = {

    className: PropTypes.string,
    style: PropTypes.object,

    /**
     * Children passed into table header.
     */
    columns: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.shape({

        /**
         * fixed position of column ( true / 'left' / 'right' ).
         */
        fixed: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(Util.enumerateValue(HorizontalAlign))]),

        /**
         * width of column.
         */
        width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

        /**
         * minimum width of column.
         */
        minWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

        /**
         * align of current column.
         */
        align: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * no wrap of current column.
         */
        noWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of header.
         */
        headClassName: PropTypes.string,

        /**
         * Override the styles of header.
         */
        headStyle: PropTypes.object,

        /**
         * align of table header cell
         */
        headAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table head.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        headRenderer: PropTypes.any,

        /**
         * column span of table header.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        headSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table header.
         */
        headNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of td.
         */
        bodyClassName: PropTypes.string,

        /**
         * Override the styles of td.
         */
        bodyStyle: PropTypes.object,

        /**
         * align of table body cell.
         */
        bodyAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table body.
         *  (1) callback:
         *      function (rowData, rowIndex, colIndex) {
         *          return rowData.id;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        bodyRenderer: PropTypes.any,

        /**
         * column span of table body.
         *  (1) function callback:
         *      function (rowData, colIndex, rowIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        bodySpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table body.
         */
        bodyNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of footer.
         */
        footClassName: PropTypes.string,

        /**
         * Override the styles of footer.
         */
        footStyle: PropTypes.object,

        /**
         * align of table footer cell
         */
        footAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table foot.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        footRenderer: PropTypes.any,

        /**
         * column span of table foot.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        footSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table foot.
         */
        footNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * If true,this column can be sorted.
         */
        sortable: PropTypes.bool,

        /**
         * Sorting property.
         */
        sortingProp: PropTypes.string,

        defaultSortingType: PropTypes.oneOf(Util.enumerateValue(SortingType))

    }))).isRequired,

    data: PropTypes.array,
    baseColIndex: PropTypes.number,

    /**
     * sorting
     */
    sorting: PropTypes.shape({
        prop: PropTypes.string,
        type: PropTypes.oneOf(Util.enumerateValue(SortingType))
    }),
    defaultSortingType: PropTypes.oneOf(Util.enumerateValue(SortingType)),
    sortingAscIconCls: PropTypes.string,
    sortingDescIconCls: PropTypes.string,

    onSortChange: PropTypes.func

};

Thead.defaultProps = {

    baseColIndex: 0,

    defaultSortingType: SortingType.ASC,
    sortingAscIconCls: 'fas fa-sort-up',
    sortingDescIconCls: 'fas fa-sort-down'

};

export default Thead;
<|endoftext|>


<commit_before>import { useReducer, useRef, useState } from 'react';

import PropTypes from 'prop-types';

import { updateServerTime } from '@proton/crypto';
import configureApi from '@proton/shared/lib/api';
import {
    getApiError,
    getApiErrorMessage,
    getIsOfflineError,
    getIsUnreachableError,
} from '@proton/shared/lib/api/helpers/apiErrorHelper';
import withApiHandlers from '@proton/shared/lib/api/helpers/withApiHandlers';
import { getClientID } from '@proton/shared/lib/apps/helper';
import xhr from '@proton/shared/lib/fetch/fetch';
import { withLocaleHeaders } from '@proton/shared/lib/fetch/headers';
import { getDateHeader } from '@proton/shared/lib/fetch/helpers';
import { localeCode } from '@proton/shared/lib/i18n';

import { useModals, useNotifications } from '../../hooks';
import UnlockModal from '../login/UnlockModal';
import AuthModal from '../password/AuthModal';
import DelinquentModal from './DelinquentModal';
import ApiContext from './apiContext';
import ApiServerTimeContext from './apiServerTimeContext';
import ApiStatusContext, { defaultApiStatus } from './apiStatusContext';
import HumanVerificationModal from './humanVerification/HumanVerificationModal';

const getSilenced = ({ silence } = {}, code) => {
    if (Array.isArray(silence)) {
        return silence.includes(code);
    }
    return !!silence;
};

const reducer = (oldState, diff) => {
    const newState = {
        ...oldState,
        ...diff,
    };
    // To prevent rerenders
    if (Object.keys(newState).every((key) => oldState[key] === newState[key])) {
        return oldState;
    }
    return newState;
};

/** @type any */
const ApiProvider = ({ config, onLogout, children, UID, noErrorState }) => {
    const { createNotification } = useNotifications();
    const { createModal } = useModals();
    const [apiStatus, setApiStatus] = useReducer(reducer, defaultApiStatus);
    const [apiServerTime, setApiServerTime] = useState(undefined);
    const apiRef = useRef();

    if (!apiRef.current) {
        const handleMissingScopes = ({ scopes: missingScopes = [], error, options }) => {
            if (missingScopes.includes('nondelinquent')) {
                return new Promise((resolve, reject) => {
                    createModal(
                        <DelinquentModal
                            onClose={() => {
                                error.cancel = true;
                                reject(error);
                            }}
                        />
                    );
                });
            }
            if (missingScopes.includes('locked')) {
                return new Promise((resolve, reject) => {
                    createModal(
                        <UnlockModal
                            onCancel={() => {
                                error.cancel = true;
                                reject(error);
                            }}
                            onSuccess={() => {
                                if (!apiRef.current) {
                                    reject(error);
                                    return;
                                }
                                return resolve(apiRef.current(options));
                            }}
                        />
                    );
                });
            }
            if (missingScopes.includes('password')) {
                return new Promise((resolve, reject) => {
                    createModal(
                        <AuthModal
                            config={options}
                            onCancel={() => {
                                error.cancel = true;
                                reject(error);
                            }}
                            onError={(apiError) => {
                                reject(apiError);
                            }}
                            onSuccess={(result) => resolve(result.response)}
                        />
                    );
                });
            }
            return Promise.reject(error);
        };

        const handleVerification = ({ token, methods, onVerify, title }, error) => {
            return new Promise((resolve, reject) => {
                createModal(
                    <HumanVerificationModal
                        title={title}
                        token={token}
                        methods={methods}
                        onVerify={onVerify}
                        onSuccess={resolve}
                        onError={reject}
                        onClose={() => {
                            error.cancel = true;
                            reject(error);
                        }}
                    />
                );
            });
        };

        const call = configureApi({
            ...config,
            clientID: getClientID(config.APP_NAME),
            xhr,
            UID,
        });

        const callWithApiHandlers = withApiHandlers({
            call,
            UID,
            onMissingScopes: handleMissingScopes,
            onVerification: handleVerification,
        });

        apiRef.current = ({ output = 'json', ...rest }) => {
            // Only need to send locale headers in public app
            const config = UID ? rest : withLocaleHeaders(localeCode, rest);
            return callWithApiHandlers(config)
                .then((response) => {
                    const serverTime = getDateHeader(response.headers);
                    if (!serverTime) {
                        // The HTTP Date header is mandatory, so this should never occur.
                        // We need the server time for proper time sync:
                        // falling back to the local time can result in e.g. unverifiable signatures
                        throw new Error('Could not fetch server time');
                    }
                    setApiServerTime(updateServerTime(serverTime));
                    setApiStatus(defaultApiStatus);

                    if (output === 'stream') {
                        return response.body;
                    }
                    if (output === 'raw') {
                        return response;
                    }
                    return response[output]();
                })
                .catch((e) => {
                    const serverTime = e.response?.headers ? getDateHeader(e.response.headers) : undefined;
                    if (serverTime) {
                        setApiServerTime(updateServerTime(serverTime));
                    }

                    const { code } = getApiError(e);
                    const errorMessage = getApiErrorMessage(e);

                    const handleErrorNotification = () => {
                        if (errorMessage) {
                            const isSilenced = getSilenced(e.config, code);
                            if (!isSilenced) {
                                createNotification({
                                    type: 'error',
                                    expiration: config?.notificationExpiration,
                                    text: errorMessage,
                                });
                            }
                        }
                    };

                    // Intended for the verify app where we always want to pass an error notification
                    if (noErrorState) {
                        handleErrorNotification();
                        throw e;
                    }

                    const isOffline = getIsOfflineError(e);
                    const isUnreachable = getIsUnreachableError(e);

                    if (isOffline || isUnreachable) {
                        setApiStatus({
                            apiUnreachable: isUnreachable ? errorMessage : '',
                            offline: isOffline,
                        });
                        throw e;
                    }
                    setApiStatus({
                        apiUnreachable: defaultApiStatus.apiUnreachable,
                        offline: defaultApiStatus.offline,
                    });

                    if (e.name === 'AbortError' || e.cancel) {
                        throw e;
                    }

                    if (e.name === 'AppVersionBadError') {
                        setApiStatus({ appVersionBad: true });
                        throw e;
                    }

                    if (e.name === 'InactiveSession') {
                        onLogout();
                        throw e;
                    }

                    handleErrorNotification();
                    throw e;
                });
        };
    }

    return (
        <ApiContext.Provider value={apiRef.current}>
            <ApiStatusContext.Provider value={apiStatus}>
                <ApiServerTimeContext.Provider value={apiServerTime}>{children}</ApiServerTimeContext.Provider>
            </ApiStatusContext.Provider>
        </ApiContext.Provider>
    );
};

ApiProvider.propTypes = {
    children: PropTypes.node.isRequired,
    config: PropTypes.object.isRequired,
    noErrorState: PropTypes.bool,
    UID: PropTypes.string,
    onLogout: PropTypes.func,
};

export default ApiProvider;
<commit_msg>Fix incorrect response in unlock<commit_after>import { useReducer, useRef, useState } from 'react';

import PropTypes from 'prop-types';

import { updateServerTime } from '@proton/crypto';
import configureApi from '@proton/shared/lib/api';
import {
    getApiError,
    getApiErrorMessage,
    getIsOfflineError,
    getIsUnreachableError,
} from '@proton/shared/lib/api/helpers/apiErrorHelper';
import withApiHandlers from '@proton/shared/lib/api/helpers/withApiHandlers';
import { getClientID } from '@proton/shared/lib/apps/helper';
import xhr from '@proton/shared/lib/fetch/fetch';
import { withLocaleHeaders } from '@proton/shared/lib/fetch/headers';
import { getDateHeader } from '@proton/shared/lib/fetch/helpers';
import { localeCode } from '@proton/shared/lib/i18n';

import { useModals, useNotifications } from '../../hooks';
import UnlockModal from '../login/UnlockModal';
import AuthModal from '../password/AuthModal';
import DelinquentModal from './DelinquentModal';
import ApiContext from './apiContext';
import ApiServerTimeContext from './apiServerTimeContext';
import ApiStatusContext, { defaultApiStatus } from './apiStatusContext';
import HumanVerificationModal from './humanVerification/HumanVerificationModal';

const getSilenced = ({ silence } = {}, code) => {
    if (Array.isArray(silence)) {
        return silence.includes(code);
    }
    return !!silence;
};

const reducer = (oldState, diff) => {
    const newState = {
        ...oldState,
        ...diff,
    };
    // To prevent rerenders
    if (Object.keys(newState).every((key) => oldState[key] === newState[key])) {
        return oldState;
    }
    return newState;
};

/** @type any */
const ApiProvider = ({ config, onLogout, children, UID, noErrorState }) => {
    const { createNotification } = useNotifications();
    const { createModal } = useModals();
    const [apiStatus, setApiStatus] = useReducer(reducer, defaultApiStatus);
    const [apiServerTime, setApiServerTime] = useState(undefined);
    const apiRef = useRef();

    if (!apiRef.current) {
        const handleMissingScopes = ({ scopes: missingScopes = [], error, options }) => {
            if (missingScopes.includes('nondelinquent')) {
                return new Promise((resolve, reject) => {
                    createModal(
                        <DelinquentModal
                            onClose={() => {
                                error.cancel = true;
                                reject(error);
                            }}
                        />
                    );
                });
            }
            if (missingScopes.includes('locked')) {
                return new Promise((resolve, reject) => {
                    createModal(
                        <UnlockModal
                            onCancel={() => {
                                error.cancel = true;
                                reject(error);
                            }}
                            onSuccess={() => {
                                if (!apiRef.current) {
                                    reject(error);
                                    return;
                                }
                                return resolve(apiRef.current({ ...options, output: 'raw' }));
                            }}
                        />
                    );
                });
            }
            if (missingScopes.includes('password')) {
                return new Promise((resolve, reject) => {
                    createModal(
                        <AuthModal
                            config={options}
                            onCancel={() => {
                                error.cancel = true;
                                reject(error);
                            }}
                            onError={(apiError) => {
                                reject(apiError);
                            }}
                            onSuccess={(result) => resolve(result.response)}
                        />
                    );
                });
            }
            return Promise.reject(error);
        };

        const handleVerification = ({ token, methods, onVerify, title }, error) => {
            return new Promise((resolve, reject) => {
                createModal(
                    <HumanVerificationModal
                        title={title}
                        token={token}
                        methods={methods}
                        onVerify={onVerify}
                        onSuccess={resolve}
                        onError={reject}
                        onClose={() => {
                            error.cancel = true;
                            reject(error);
                        }}
                    />
                );
            });
        };

        const call = configureApi({
            ...config,
            clientID: getClientID(config.APP_NAME),
            xhr,
            UID,
        });

        const callWithApiHandlers = withApiHandlers({
            call,
            UID,
            onMissingScopes: handleMissingScopes,
            onVerification: handleVerification,
        });

        apiRef.current = ({ output = 'json', ...rest }) => {
            // Only need to send locale headers in public app
            const config = UID ? rest : withLocaleHeaders(localeCode, rest);
            return callWithApiHandlers(config)
                .then((response) => {
                    const serverTime = getDateHeader(response.headers);
                    if (!serverTime) {
                        // The HTTP Date header is mandatory, so this should never occur.
                        // We need the server time for proper time sync:
                        // falling back to the local time can result in e.g. unverifiable signatures
                        throw new Error('Could not fetch server time');
                    }
                    setApiServerTime(updateServerTime(serverTime));
                    setApiStatus(defaultApiStatus);

                    if (output === 'stream') {
                        return response.body;
                    }
                    if (output === 'raw') {
                        return response;
                    }
                    return response[output]();
                })
                .catch((e) => {
                    const serverTime = e.response?.headers ? getDateHeader(e.response.headers) : undefined;
                    if (serverTime) {
                        setApiServerTime(updateServerTime(serverTime));
                    }

                    const { code } = getApiError(e);
                    const errorMessage = getApiErrorMessage(e);

                    const handleErrorNotification = () => {
                        if (errorMessage) {
                            const isSilenced = getSilenced(e.config, code);
                            if (!isSilenced) {
                                createNotification({
                                    type: 'error',
                                    expiration: config?.notificationExpiration,
                                    text: errorMessage,
                                });
                            }
                        }
                    };

                    // Intended for the verify app where we always want to pass an error notification
                    if (noErrorState) {
                        handleErrorNotification();
                        throw e;
                    }

                    const isOffline = getIsOfflineError(e);
                    const isUnreachable = getIsUnreachableError(e);

                    if (isOffline || isUnreachable) {
                        setApiStatus({
                            apiUnreachable: isUnreachable ? errorMessage : '',
                            offline: isOffline,
                        });
                        throw e;
                    }
                    setApiStatus({
                        apiUnreachable: defaultApiStatus.apiUnreachable,
                        offline: defaultApiStatus.offline,
                    });

                    if (e.name === 'AbortError' || e.cancel) {
                        throw e;
                    }

                    if (e.name === 'AppVersionBadError') {
                        setApiStatus({ appVersionBad: true });
                        throw e;
                    }

                    if (e.name === 'InactiveSession') {
                        onLogout();
                        throw e;
                    }

                    handleErrorNotification();
                    throw e;
                });
        };
    }

    return (
        <ApiContext.Provider value={apiRef.current}>
            <ApiStatusContext.Provider value={apiStatus}>
                <ApiServerTimeContext.Provider value={apiServerTime}>{children}</ApiServerTimeContext.Provider>
            </ApiStatusContext.Provider>
        </ApiContext.Provider>
    );
};

ApiProvider.propTypes = {
    children: PropTypes.node.isRequired,
    config: PropTypes.object.isRequired,
    noErrorState: PropTypes.bool,
    UID: PropTypes.string,
    onLogout: PropTypes.func,
};

export default ApiProvider;
<|endoftext|>


<commit_before>const Service = require("moleculer").Service;
const DbService = require("moleculer-db");
const { ValidationError } = require("moleculer").Errors;
const _ = require('lodash');
const getDbAdapterConfig = require("../lib/getDbAdapterConfig.js");
const fs = require('fs');
const path = require('path');
const RankTable = require('./league/lib/RankTable.js');
const JavaScriptObfuscator = require('javascript-obfuscator');
const stripComments = require('strip-comments');
const validators = require("../validators");

class LeagueService extends Service {

  constructor(broker) {
    super(broker);
    this.ranktable = new RankTable();
    this.config = broker.serviceConfig.league;
    let adapterConfig = getDbAdapterConfig(broker.serviceConfig.data, 'league')
    this.parseServiceSchema({
      ...adapterConfig,
      name: "league",
      mixins: [DbService],
      settings: {
        idField: 'id',
        fields: [
          "id",
          "joinedAt",
          "ownerId",
          "ownerName",
          "scriptId",
          "scriptName",
          "fights_total",
          "fights_win",
          "fights_lose",
          "fights_error",
          "score",
          "code",
          "hash"
        ]
      },
      entityValidator: {
        id: validators.entityId({optional: true}),
        joinedAt: validators.createDate(),
        ownerId: validators.entityId(),
        ownerName: validators.entityName(),
        scriptId: validators.entityId(),
        scriptName: validators.entityName(),
        fights_total: {type: "number", positive: true},
        fights_win: {type: "number", positive: true},
        fights_lose: {type: "number", positive: true},
        fights_error: {type: "number", positive: true},
        score: {type: "number", positive: true},
        code: validators.code(),
        hash: validators.hash()
      },
      actions: {
        pickRandomOpponents: this.pickRandomOpponents,
        seedLeague: this.seedLeague,
        getUserSubmission: this.getUserSubmission,
        getHistory: this.getHistory,
        leaveLeague: this.leaveLeague,
        getLeagueSummary: this.getLeagueSummary,
        getUserRankTable: this.getUserRankTable,
        getScript: {
          params: {
            id: validators.entityId()
          },
          handler: this.getScript
        },
        joinLeague: {
          scriptId: validators.entityId(),
          handler: this.joinLeague
        },
        updateRank: {
          params: {
            id: validators.entityId(),
            winner: { type: "boolean" }
          },
          handler: this.updateRank
        },
        listRankTable: {
          params: {
            page: {type: "number", positive: true, min: 1, optional: true, convert: true},
            pageSize: {type: "number", positive: true, min: 1, max: 50, optional: true, convert: true}
          },
          handler: this.listRankTable
        },
      },
      hooks: {
        before: {
          create: [
            function addDefaults(ctx) {
              ctx.params.joinedAt = new Date();
              ctx.params.fights_total = 0;
              ctx.params.fights_win = 0;
              ctx.params.fights_lose = 0;
              ctx.params.fights_error = 0;
              ctx.params.score = ctx.params.score || 0;
              ctx.params = _.omit(ctx.params, ['id']);
              return ctx;
            }
          ]
        }
      },
      events: {
        "app.seed": async (ctx) => {
          await ctx.call('league.seedLeague', {})

          this.logger.info('Initializing Rank able');
          let initData = await ctx.call('league.find', {
            sort: '-score',
            fields: [
              "id",
              "ownerId",
              "ownerName",
              "scriptId",
              "scriptName",
              "joinedAt",
              "fights_total",
              "fights_win",
              "fights_lose",
              "fights_error",
              "score"
            ]
          });
          this.ranktable.init(initData);
          this.logger.info('Rank Table initialized');
        }
      }
    });
  }

  async getScript(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    const scriptId = ctx.params.id
    let response = await ctx.call('league.get', {
      id: scriptId,
      fields: [
        "id",
        "ownerName",
        "scriptName",
        "code"
      ]
    });

    return {
      id: response.id,
      scriptName: response.ownerName + '/' + response.scriptName,
      code: response.code
    };
  }

  async getHistory(ctx) {
    let items = await ctx.call('battleStore.find', {
      sort: '-createdAt',
      limit: 7,
      fields: [
        "id",
        "description",
        "meta",
        "createdAt"
      ]
    });
    items = items.map((item) => ({
      id: item.id,
      createdAt: item.createdAt,
      players: item.meta.map((player) => ({
        id: player.id,
        name: player.name,
        winner: player.winner
      }))
    }))
    return items;
  }

  async updateRank(ctx) {
    let entity = await this._get(ctx, {
      id: ctx.params.id,
      fields: [
        'id',
        'fights_total',
        'fights_win',
        'fights_lose',
        'score'
      ]
    });

    let newScore;
    if(ctx.params.winner) {
      newScore = entity.score + (10000 - entity.score)/25;
    } else {
      newScore = entity.score + (0 - entity.score)/25;
    }
    let newEntity = {
      id: entity.id,
      fights_total: entity.fights_total + 1,
      fights_win: entity.fights_win + (ctx.params.winner ? 1 : 0),
      fights_lose: entity.fights_lose + (ctx.params.winner ? 0 : 1),
      score: Math.round(newScore)
    }
    this._update(ctx, newEntity);
    this.ranktable.updateScore(
      newEntity.id,
      newEntity.score,
      newEntity.fights_total,
      newEntity.fights_win,
      newEntity.fights_lose
    );
  }

  async pickRandomOpponents(ctx) {
    let opponents = this.ranktable.pickRandom();
    let opponent1 = await ctx.call('league.get', {id: opponents[0].id})
    let opponent2 = await ctx.call('league.get', {id: opponents[1].id})
    return [
      opponent1,
      opponent2
    ]
  }

  async seedLeague(ctx) {
    const seedPath = path.resolve(__dirname, 'league', 'seed');
    const seedFiles = fs.readdirSync(seedPath)
      .map((filename, index) => ({
        ownerId: 'int-user-0000-1',
        ownerName: 'jsbattle',
        scriptId: 'int-script-0000-' + (index+1),
        scriptName: filename.replace(/\.tank$/, ''),
        code: fs.readFileSync(path.resolve(seedPath, filename), 'utf8')
      }))
      .map((entry) => new Promise(async (resolve) => {
        let existingEntry = await ctx.call('league.find', {
          query: {
            ownerName: 'jsbattle',
            scriptName: entry.scriptName,
          }
        });
        if(existingEntry.length == 0) {
          await ctx.call('league.create', entry);
        }
        resolve();
      }));

    await Promise.all(seedFiles);
  }

  async getUserSubmission(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let leagueEntry = await ctx.call('league.find', {
      query: {
        ownerId: userId
      },
      limit: 1
    });

    if(leagueEntry.length === 0) {
      return {}
    }
    leagueEntry = leagueEntry[0];
    let items = await ctx.call('battleStore.find', {
      query: {
        owner: {$in: [leagueEntry.id]}
      },
      sort: '-createdAt',
      limit: 10,
      fields: [
        "id",
        "meta",
        "createdAt"
      ]
    });
    items = items.map((item) => ({
      id: item.id,
      createdAt: item.createdAt,
      players: item.meta.map((player) => ({
        id: player.id,
        name: player.name,
        winner: player.winner
      }))
    }));
    leagueEntry.history = items;

    leagueEntry.latest = true;
    try {
      let script = await ctx.call('scriptStore.getUserScript', { id: leagueEntry.scriptId});
      if(script && script.hash !== leagueEntry.hash) {
        leagueEntry.latest = false;
      }
    } catch (err) {
      this.logger.debug('Reference script not found')
    }

    return leagueEntry;
  }

  async joinLeague(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let script = await ctx.call('scriptStore.getUserScript', { id: ctx.params.scriptId });

    if(script.ownerId != userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let currentSubmission = await ctx.call('league.getUserSubmission', {});
    let startingScore = 0;
    if(ctx.params.scriptId === currentSubmission.scriptId) {
      startingScore = currentSubmission.score;
    }

    await this.leaveLeague(ctx);

    let code = script.code;
    if(this.config.obfuscate) {
      try {
        let prevSize = Math.round(code.length/1024);
        code = stripComments(code);
        code = code.replace(/importScripts\w*\([^)]*\)/g, '');
        code = JavaScriptObfuscator.obfuscate(code, {
          compact: true,
          controlFlowFlattening: false,
          deadCodeInjection: false,
          debugProtection: false,
          debugProtectionInterval: false,
          disableConsoleOutput: true,
          identifierNamesGenerator: 'hexadecimal',
          log: false,
          renameGlobals: false,
          rotateStringArray: true,
          selfDefending: true,
          shuffleStringArray: true,
          splitStrings: false,
          stringArray: true,
          stringArrayEncoding: false,
          stringArrayThreshold: 0.75,
          unicodeEscapeSequence: false
        }).getObfuscatedCode();
        let currentSize = Math.round(code.length/1024);
        this.logger.info(`Code obfuscated ${prevSize}K -> ${currentSize}K`);

      } catch (err) {
        this.logger.warn(err);
      }
    } else {
      this.logger.info(`Code obfuscation disabled`);
    }

    const entity = await ctx.call('league.create', {
      ownerId: script.ownerId,
      ownerName: script.ownerName,
      scriptId: script.id,
      scriptName: script.scriptName,
      code: code,
      hash: script.hash,
      score: startingScore
    });

    this.ranktable.add({
      id: entity.id,
      ownerId: entity.ownerId,
      ownerName: entity.ownerName,
      scriptId: entity.scriptId,
      scriptName: entity.scriptName,
      joinedAt: entity.joinedAt,
      fights_total: entity.fights_total,
      fights_win: entity.fights_win,
      fights_lose: entity.fights_lose,
      fights_error: entity.fights_error,
      score: entity.score
    });

    return this.getLeagueSummary(ctx);
  }

  async leaveLeague(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let submissions = await ctx.call('league.find', {
      query: {
        ownerId: userId
      }
    });

    let removals = submissions.map((submission) => ctx.call('league.remove', {
        id: submission.id
    }));

    for(let submission of submissions) {
      this.ranktable.remove(submission.id)
    }

    await Promise.all(removals);

    return this.getLeagueSummary(ctx);
  }

  async getUserRankTable(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    const fields = [
      "id",
      "ownerId",
      "ownerName",
      "scriptId",
      "scriptName",
      "joinedAt",
      "fights_total",
      "fights_win",
      "fights_lose",
      "fights_error",
      "hash",
      "score",
      "latest",
      "history"
    ];

    let submission = await this.getUserSubmission(ctx);
    submission = _.pick(submission, fields);

    return {
      submission,
      ranktable: this.ranktable.slice(submission.id, 7)
    }
  }

  async getLeagueSummary(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let result = await this.getUserRankTable(ctx);
    return {
      ...result,
      history: await ctx.call('league.getHistory', {})
    }
  }

  listRankTable(ctx) {
    let page = ctx.params.page || 1;
    let pageSize = ctx.params.pageSize || 10;
    let total = this.ranktable.getLength();
    let totalPages = Math.ceil(total/pageSize);
    let offset = (page-1)*pageSize;
    let rows = this.ranktable.getData().slice(offset, offset+pageSize);
    return {
      rows,
      page,
      pageSize,
      total,
      totalPages
    }
  }
}
module.exports = LeagueService;
<commit_msg>improvement(jsbattle-server): extend league preview list<commit_after>const Service = require("moleculer").Service;
const DbService = require("moleculer-db");
const { ValidationError } = require("moleculer").Errors;
const _ = require('lodash');
const getDbAdapterConfig = require("../lib/getDbAdapterConfig.js");
const fs = require('fs');
const path = require('path');
const RankTable = require('./league/lib/RankTable.js');
const JavaScriptObfuscator = require('javascript-obfuscator');
const stripComments = require('strip-comments');
const validators = require("../validators");

class LeagueService extends Service {

  constructor(broker) {
    super(broker);
    this.ranktable = new RankTable();
    this.config = broker.serviceConfig.league;
    let adapterConfig = getDbAdapterConfig(broker.serviceConfig.data, 'league')
    this.parseServiceSchema({
      ...adapterConfig,
      name: "league",
      mixins: [DbService],
      settings: {
        idField: 'id',
        fields: [
          "id",
          "joinedAt",
          "ownerId",
          "ownerName",
          "scriptId",
          "scriptName",
          "fights_total",
          "fights_win",
          "fights_lose",
          "fights_error",
          "score",
          "code",
          "hash"
        ]
      },
      entityValidator: {
        id: validators.entityId({optional: true}),
        joinedAt: validators.createDate(),
        ownerId: validators.entityId(),
        ownerName: validators.entityName(),
        scriptId: validators.entityId(),
        scriptName: validators.entityName(),
        fights_total: {type: "number", positive: true},
        fights_win: {type: "number", positive: true},
        fights_lose: {type: "number", positive: true},
        fights_error: {type: "number", positive: true},
        score: {type: "number", positive: true},
        code: validators.code(),
        hash: validators.hash()
      },
      actions: {
        pickRandomOpponents: this.pickRandomOpponents,
        seedLeague: this.seedLeague,
        getUserSubmission: this.getUserSubmission,
        getHistory: this.getHistory,
        leaveLeague: this.leaveLeague,
        getLeagueSummary: this.getLeagueSummary,
        getUserRankTable: this.getUserRankTable,
        getScript: {
          params: {
            id: validators.entityId()
          },
          handler: this.getScript
        },
        joinLeague: {
          scriptId: validators.entityId(),
          handler: this.joinLeague
        },
        updateRank: {
          params: {
            id: validators.entityId(),
            winner: { type: "boolean" }
          },
          handler: this.updateRank
        },
        listRankTable: {
          params: {
            page: {type: "number", positive: true, min: 1, optional: true, convert: true},
            pageSize: {type: "number", positive: true, min: 1, max: 50, optional: true, convert: true}
          },
          handler: this.listRankTable
        },
      },
      hooks: {
        before: {
          create: [
            function addDefaults(ctx) {
              ctx.params.joinedAt = new Date();
              ctx.params.fights_total = 0;
              ctx.params.fights_win = 0;
              ctx.params.fights_lose = 0;
              ctx.params.fights_error = 0;
              ctx.params.score = ctx.params.score || 0;
              ctx.params = _.omit(ctx.params, ['id']);
              return ctx;
            }
          ]
        }
      },
      events: {
        "app.seed": async (ctx) => {
          await ctx.call('league.seedLeague', {})

          this.logger.info('Initializing Rank able');
          let initData = await ctx.call('league.find', {
            sort: '-score',
            fields: [
              "id",
              "ownerId",
              "ownerName",
              "scriptId",
              "scriptName",
              "joinedAt",
              "fights_total",
              "fights_win",
              "fights_lose",
              "fights_error",
              "score"
            ]
          });
          this.ranktable.init(initData);
          this.logger.info('Rank Table initialized');
        }
      }
    });
  }

  async getScript(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    const scriptId = ctx.params.id
    let response = await ctx.call('league.get', {
      id: scriptId,
      fields: [
        "id",
        "ownerName",
        "scriptName",
        "code"
      ]
    });

    return {
      id: response.id,
      scriptName: response.ownerName + '/' + response.scriptName,
      code: response.code
    };
  }

  async getHistory(ctx) {
    let items = await ctx.call('battleStore.find', {
      sort: '-createdAt',
      limit: 7,
      fields: [
        "id",
        "description",
        "meta",
        "createdAt"
      ]
    });
    items = items.map((item) => ({
      id: item.id,
      createdAt: item.createdAt,
      players: item.meta.map((player) => ({
        id: player.id,
        name: player.name,
        winner: player.winner
      }))
    }))
    return items;
  }

  async updateRank(ctx) {
    let entity = await this._get(ctx, {
      id: ctx.params.id,
      fields: [
        'id',
        'fights_total',
        'fights_win',
        'fights_lose',
        'score'
      ]
    });

    let newScore;
    if(ctx.params.winner) {
      newScore = entity.score + (10000 - entity.score)/25;
    } else {
      newScore = entity.score + (0 - entity.score)/25;
    }
    let newEntity = {
      id: entity.id,
      fights_total: entity.fights_total + 1,
      fights_win: entity.fights_win + (ctx.params.winner ? 1 : 0),
      fights_lose: entity.fights_lose + (ctx.params.winner ? 0 : 1),
      score: Math.round(newScore)
    }
    this._update(ctx, newEntity);
    this.ranktable.updateScore(
      newEntity.id,
      newEntity.score,
      newEntity.fights_total,
      newEntity.fights_win,
      newEntity.fights_lose
    );
  }

  async pickRandomOpponents(ctx) {
    let opponents = this.ranktable.pickRandom();
    let opponent1 = await ctx.call('league.get', {id: opponents[0].id})
    let opponent2 = await ctx.call('league.get', {id: opponents[1].id})
    return [
      opponent1,
      opponent2
    ]
  }

  async seedLeague(ctx) {
    const seedPath = path.resolve(__dirname, 'league', 'seed');
    const seedFiles = fs.readdirSync(seedPath)
      .map((filename, index) => ({
        ownerId: 'int-user-0000-1',
        ownerName: 'jsbattle',
        scriptId: 'int-script-0000-' + (index+1),
        scriptName: filename.replace(/\.tank$/, ''),
        code: fs.readFileSync(path.resolve(seedPath, filename), 'utf8')
      }))
      .map((entry) => new Promise(async (resolve) => {
        let existingEntry = await ctx.call('league.find', {
          query: {
            ownerName: 'jsbattle',
            scriptName: entry.scriptName,
          }
        });
        if(existingEntry.length == 0) {
          await ctx.call('league.create', entry);
        }
        resolve();
      }));

    await Promise.all(seedFiles);
  }

  async getUserSubmission(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let leagueEntry = await ctx.call('league.find', {
      query: {
        ownerId: userId
      },
      limit: 1
    });

    if(leagueEntry.length === 0) {
      return {}
    }
    leagueEntry = leagueEntry[0];
    let items = await ctx.call('battleStore.find', {
      query: {
        owner: {$in: [leagueEntry.id]}
      },
      sort: '-createdAt',
      limit: 10,
      fields: [
        "id",
        "meta",
        "createdAt"
      ]
    });
    items = items.map((item) => ({
      id: item.id,
      createdAt: item.createdAt,
      players: item.meta.map((player) => ({
        id: player.id,
        name: player.name,
        winner: player.winner
      }))
    }));
    leagueEntry.history = items;

    leagueEntry.latest = true;
    try {
      let script = await ctx.call('scriptStore.getUserScript', { id: leagueEntry.scriptId});
      if(script && script.hash !== leagueEntry.hash) {
        leagueEntry.latest = false;
      }
    } catch (err) {
      this.logger.debug('Reference script not found')
    }

    return leagueEntry;
  }

  async joinLeague(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let script = await ctx.call('scriptStore.getUserScript', { id: ctx.params.scriptId });

    if(script.ownerId != userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let currentSubmission = await ctx.call('league.getUserSubmission', {});
    let startingScore = 0;
    if(ctx.params.scriptId === currentSubmission.scriptId) {
      startingScore = currentSubmission.score;
    }

    await this.leaveLeague(ctx);

    let code = script.code;
    if(this.config.obfuscate) {
      try {
        let prevSize = Math.round(code.length/1024);
        code = stripComments(code);
        code = code.replace(/importScripts\w*\([^)]*\)/g, '');
        code = JavaScriptObfuscator.obfuscate(code, {
          compact: true,
          controlFlowFlattening: false,
          deadCodeInjection: false,
          debugProtection: false,
          debugProtectionInterval: false,
          disableConsoleOutput: true,
          identifierNamesGenerator: 'hexadecimal',
          log: false,
          renameGlobals: false,
          rotateStringArray: true,
          selfDefending: true,
          shuffleStringArray: true,
          splitStrings: false,
          stringArray: true,
          stringArrayEncoding: false,
          stringArrayThreshold: 0.75,
          unicodeEscapeSequence: false
        }).getObfuscatedCode();
        let currentSize = Math.round(code.length/1024);
        this.logger.info(`Code obfuscated ${prevSize}K -> ${currentSize}K`);

      } catch (err) {
        this.logger.warn(err);
      }
    } else {
      this.logger.info(`Code obfuscation disabled`);
    }

    const entity = await ctx.call('league.create', {
      ownerId: script.ownerId,
      ownerName: script.ownerName,
      scriptId: script.id,
      scriptName: script.scriptName,
      code: code,
      hash: script.hash,
      score: startingScore
    });

    this.ranktable.add({
      id: entity.id,
      ownerId: entity.ownerId,
      ownerName: entity.ownerName,
      scriptId: entity.scriptId,
      scriptName: entity.scriptName,
      joinedAt: entity.joinedAt,
      fights_total: entity.fights_total,
      fights_win: entity.fights_win,
      fights_lose: entity.fights_lose,
      fights_error: entity.fights_error,
      score: entity.score
    });

    return this.getLeagueSummary(ctx);
  }

  async leaveLeague(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let submissions = await ctx.call('league.find', {
      query: {
        ownerId: userId
      }
    });

    let removals = submissions.map((submission) => ctx.call('league.remove', {
        id: submission.id
    }));

    for(let submission of submissions) {
      this.ranktable.remove(submission.id)
    }

    await Promise.all(removals);

    return this.getLeagueSummary(ctx);
  }

  async getUserRankTable(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    const fields = [
      "id",
      "ownerId",
      "ownerName",
      "scriptId",
      "scriptName",
      "joinedAt",
      "fights_total",
      "fights_win",
      "fights_lose",
      "fights_error",
      "hash",
      "score",
      "latest",
      "history"
    ];

    let submission = await this.getUserSubmission(ctx);
    submission = _.pick(submission, fields);

    return {
      submission,
      ranktable: this.ranktable.slice(submission.id, 9)
    }
  }

  async getLeagueSummary(ctx) {
    const userId = ctx.meta.user ? ctx.meta.user.id : null;
    if(!userId) {
      throw new ValidationError('Not Authorized!', 401);
    }

    let result = await this.getUserRankTable(ctx);
    return {
      ...result,
      history: await ctx.call('league.getHistory', {})
    }
  }

  listRankTable(ctx) {
    let page = ctx.params.page || 1;
    let pageSize = ctx.params.pageSize || 10;
    let total = this.ranktable.getLength();
    let totalPages = Math.ceil(total/pageSize);
    let offset = (page-1)*pageSize;
    let rows = this.ranktable.getData().slice(offset, offset+pageSize);
    return {
      rows,
      page,
      pageSize,
      total,
      totalPages
    }
  }
}
module.exports = LeagueService;
<|endoftext|>


<commit_before>const LRU = cjsRequire('lru-cache')
const { IndexedCramFile, CraiIndex } = cjsRequire('@gmod/cram')

const { Buffer } = cjsRequire('buffer')

const cramIndexedFilesCache = LRU(5)

const BlobFilehandleWrapper = cjsRequire('../../Model/BlobFilehandleWrapper')
const MAX_INSERT_SIZE_FOR_STATS = 30000

class PairedCramRead {
    id() {
        return Math.min(this.read1.id(), this.read2.id())
    }
    get(field) {
        return this._get(field.toLowerCase())
    }
    _get(field) {
        if(field === 'start') {
            return Math.min(this.read1._get('start'), this.read2._get('start'))
        } else if(field === 'end') {
            return Math.max(this.read1._get('end'), this.read2._get('end'))
        } else if(field === 'name') {
            return this.read1._get('name')
        } else if(field === 'pair_orientation') {
            return this.read1._get('pair_orientation')
        } else if(field === 'template_length') {
            return this.read1._get('template_length')
        }
    }
    pairedFeature() { return true }
    children() {}
}
class CramSlightlyLazyFeature {

    _get_name() { return this.record.readName }
    _get_start() { return this.record.alignmentStart-1 }
    _get_end() { return this.record.alignmentStart+this.record.lengthOnRef-1 }
    _get_cram_read_features() { return this.record.readFeatures }
    _get_type() { return 'match'}
    _get_mapping_quality() { return this.record.mappingQuality}
    _get_flags() { return `0x${this.record.flags.toString(16)}`}
    _get_cramFlags() { return `0x${this.record.cramFlags.toString(16)}`}
    _get_strand() { return this.record.isReverseComplemented() ? -1 : 1 }
    _get_read_group_id() { return this.record.readGroupId }
    _get_qual() { return (this.record.qualityScores || []).map(q => q+33).join(' ')}
    _get_seq() { return this.record.readBases}
    _get_seq_id() { return this._store._refIdToName(this.record.sequenceId)}
    _get_qc_failed() { return this.record.isFailedQc()}
    _get_secondary_alignment() { return this.record.isSecondary()}
    _get_duplicate() { return this.record.isDuplicate()}
    _get_supplementary_alignment() { return this.record.isSupplementary()}
    _get_pair_orientation() { return this.record.getPairOrientation()}
    _get_multi_segment_template() { return this.record.isPaired()}
    _get_multi_segment_all_correctly_aligned() { return this.record.isProperlyPaired()}
    _get_multi_segment_all_aligned() { return this.record.isProperlyPaired()}
    _get_multi_segment_next_segment_unmapped() { return this.record.isMateUnmapped()}
    _get_multi_segment_first() { return this.record.isRead1()}
    _get_multi_segment_last() { return this.record.isRead2()}
    _get_multi_segment_next_segment_reversed() { return this.record.isMateReverseComplemented()}
    _get_unmapped() { return this.record.isSegmentUnmapped()}
    _get_next_seq_id() { return this.record.mate ? this._store._refIdToName(this.record.mate.sequenceId) : undefined }
    _get_next_segment_position() { return this.record.mate
        ? ( this._store._refIdToName(this.record.mate.sequenceId)+':'+this.record.mate.alignmentStart) : undefined}
    _get_tags() { return this.record.tags }
    _get_seq() { return this.record.getReadBases() }

    constructor(record, store) {
        this.record = record
        this._store = store
    }

    tags() {
        const properties = Object.getOwnPropertyNames(CramSlightlyLazyFeature.prototype)
        return properties
            .filter(prop => /^_get_/.test(prop))
            .map(methodName => methodName.replace('_get_',''))
    }

    id() {
        return this.record.uniqueId + 1
    }

    _get(field) {
        const methodName = `_get_${field}`
        if (this[methodName]) return this[methodName]()
        return undefined
    }
    get(field) {
        const methodName = `_get_${field.toLowerCase()}`
        if (this[methodName]) return this[methodName]()
        return undefined
    }

    parent() {}

    children() {}

    pairedFeature() { return false }
}

function canBePaired(alignment) {
    return alignment.isPaired() &&
        !alignment.isMateUnmapped() &&
        alignment.sequenceId === alignment.mate.sequenceId &&
        (alignment.isRead1() || alignment.isRead2()) &&
        !(alignment.isSecondary() || alignment.isSupplementary());
}

define( [
            'dojo/_base/declare',
            'JBrowse/Util',
            'JBrowse/Errors',
            'JBrowse/Store/SeqFeature',
            'JBrowse/Store/DeferredStatsMixin',
            'JBrowse/Store/DeferredFeaturesMixin',
            'JBrowse/Store/SeqFeature/GlobalStatsEstimationMixin',
            'JBrowse/Model/XHRBlob',
            'JBrowse/Model/SimpleFeature',
        ],
        function(
            declare,
            Util,
            Errors,
            SeqFeatureStore,
            DeferredStatsMixin,
            DeferredFeaturesMixin,
            GlobalStatsEstimationMixin,
            XHRBlob,
            SimpleFeature,
        ) {

return declare( [ SeqFeatureStore, DeferredStatsMixin, DeferredFeaturesMixin, GlobalStatsEstimationMixin ],

/**
 * @lends JBrowse.Store.SeqFeature.CRAM
 */
{
    /**
     * Data backend for reading feature data directly from a
     * web-accessible CRAM file.
     *
     * @constructs
     */
    constructor: function( args ) {

        let dataBlob
        if (args.cram)
            dataBlob = new BlobFilehandleWrapper(args.cram)
        else if (args.urlTemplate)
            dataBlob = new BlobFilehandleWrapper(new XHRBlob(this.resolveUrl(args.urlTemplate || 'data.cram'), { expectRanges: true }))
        else throw new Error('must provide either `cram` or `urlTemplate`')

        let indexBlob
        if (args.crai)
            indexBlob = new BlobFilehandleWrapper(args.crai)
        else if (args.craiUrlTemplate)
            indexBlob = new BlobFilehandleWrapper(new XHRBlob(this.resolveUrl(args.craiUrlTemplate)))
        else if (args.urlTemplate)
            indexBlob = new BlobFilehandleWrapper(new XHRBlob(this.resolveUrl(args.urlTemplate+'.crai')))
        else throw new Error('no index provided, must provide a CRAM index')

        this.source = dataBlob.toString()

        // LRU-cache the CRAM object so we don't have to re-download the
        // index when we switch chromosomes
        const cacheKey = `data: ${dataBlob}, index: ${indexBlob}`
        this.cram = cramIndexedFilesCache.get(cacheKey)
        if (!this.cram) {
            this.cram = new IndexedCramFile({
                cramFilehandle: dataBlob,
                index: new CraiIndex({filehandle: indexBlob}),
                seqFetch: this._seqFetch.bind(this),
                checkSequenceMD5: false,
            })

            cramIndexedFilesCache.set(cacheKey, this.cram)
        }

        // pre-download the index before running the statistics estimation so that the stats
        // estimation doesn't time out
        this.cram.hasDataForReferenceSequence(0)
            .then(() => this.cram.cram.getSamHeader())
            .then(samHeader => {
                this._setSamHeader(samHeader)
            })
            .then(() => {
                this._deferred.features.resolve({success:true});
            })
            .then(() => this._estimateGlobalStats())
            .then(stats => {
                this.globalStats = stats;
                this._deferred.stats.resolve({success:true});
            })
            .catch(err => {
                this._deferred.features.reject(err)
                this._deferred.stats.reject(err)
            })

        this.storeTimeout = args.storeTimeout || 3000;
        this.featureCache = {}
    },

    // process the parsed SAM header from the cram file
    _setSamHeader(samHeader) {
        this._samHeader = {}

        // use the @SQ lines in the header to figure out the
        // mapping between ref seq ID numbers and names
        const refSeqIdToName = []
        const refSeqNameToId = {}
        const sqLines = samHeader.filter(l => l.tag === 'SQ')
        sqLines.forEach((sqLine, seqId) => {
            sqLine.data.forEach(item => {
                if (item.tag === 'SN') {
                    // this is the seq name
                    const seqName = item.value
                    refSeqNameToId[seqName] = seqId
                    refSeqIdToName[seqId] = seqName
                }
            })
        })
        if (refSeqIdToName.length) {
            this._samHeader.refSeqIdToName = refSeqIdToName
            this._samHeader.refSeqNameToId = refSeqNameToId
        }
    },

    _refNameToId(refName) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqNameToId)
            return this._samHeader.refSeqNameToId[refName]
        else
            return this.browser.getRefSeqNumber(refName)
    },

    _refIdToName(refId) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqIdToName) {
            return this._samHeader.refSeqIdToName[refId]
        } else {
            let ref = this.browser.getRefSeqById(refId)
            return ref ? ref.name : undefined
        }
    },

    _getRefSeqStore() {
        return new Promise((resolve,reject) => {
            this.browser.getStore('refseqs',resolve,reject)
        })
    },

    // used by the CRAM backend to fetch a region of the underlying reference
    // sequence.  needed for some of its calculations
    async _seqFetch(seqId, start, end) {
        start -= 1 // convert from 1-based closed to interbase

        const refSeqStore = await this._getRefSeqStore()
        if (!refSeqStore) return undefined
        const refName = this._refIdToName(seqId)
        if (!refName) return undefined

        const seqChunks = await new Promise((resolve,reject) => {
            let features = []
            refSeqStore.getFeatures(
                {ref: refName, start: start-1, end},
                f => features.push(f),
                () => resolve(features),
                reject
            )
        })

        const trimmed = []
        seqChunks
        .sort((a,b) => a.get('start') - b.get('start'))
        .forEach( (chunk,i) => {
            let chunkStart = chunk.get('start')
            let chunkEnd = chunk.get('end')
            let trimStart = Math.max(start - chunkStart, 0)
            let trimEnd = Math.min(end - chunkStart, chunkEnd-chunkStart)
            let trimLength = trimEnd - trimStart
            let chunkSeq = chunk.get('seq') || chunk.get('residues')
            trimmed.push(chunkSeq.substr(trimStart,trimLength))
        })

        const sequence = trimmed.join('')
        if (sequence.length !== (end-start))
            throw new Error(`sequence fetch failed: fetching ${
                (start-1).toLocaleString()}-${end.toLocaleString()
                } only returned ${
                    sequence.length.toLocaleString()
                } bases, but should have returned ${
                    (end-start).toLocaleString()
                }`)
        return sequence
    },

    /**
     * Interrogate whether a store has data for a given reference
     * sequence.  Calls the given callback with either true or false.
     */
    hasRefSeq: function( seqName, callback, errorCallback ) {
        seqName = this.browser.regularizeReferenceName( seqName );
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) callback(false)

        this._deferred.stats
        .then(() => this.cram.hasDataForReferenceSequence(refSeqNumber))
        .then(callback, errorCallback)
    },

    // called by getFeatures from the DeferredFeaturesMixin
    _getFeatures: function( query, featCallback, endCallback, errorCallback ) {
        const pairCache = {};
        const seqName = query.ref || this.refSeq.name
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) {
            endCallback()
            return
        }
        this.cram.getRecordsForRange(refSeqNumber, query.start + 1, query.end, {viewAsPairs: query.viewAsPairs})
            .then(records => {
                if(query.viewAsPairs) {
                    records.sort((a, b) => {
                        return (a.readName < b.readName ? -1 : (a.readName > b.readName ? 1 : 0));
                    })
                    for(let i = 0; i < records.length; i++) {
                        let feat
                        if (canBePaired(records[i])) {
                            let name = records[i].readName
                            feat = pairCache[name]
                            if (feat) {
                                if(records[i].isRead1()) {
                                    feat.read1 = this._cramRecordToFeature(records[i])
                                } else if(records[i].isRead2()) {
                                    feat.read2 = this._cramRecordToFeature(records[i])
                                } else {
                                    console.log('unable to pair read',records[i])
                                }
                                if(feat.read1 && feat.read2) {
                                    delete pairCache[name]
                                    this.featureCache[name] = feat
                                }
                            }
                            else {
                                feat = new PairedCramRead()
                                if(records[i].isRead1()) {
                                    feat.read1 = this._cramRecordToFeature(records[i])
                                } else if(records[i].isRead2()) {
                                    feat.read2 = this._cramRecordToFeature(records[i])
                                } else {
                                    console.log('unable to pair read', records[i])
                                }
                                pairCache[name] = feat
                            }
                        }
                        else if(!((records[i].alignmentStart+records[i].lengthOnRef-1) < query.start) && !(records[i].alignmentStart > query.end)){
                            let feat = this._cramRecordToFeature(records[i])
                            featCallback(feat)
                        }
                    }
                    Object.entries(this.featureCache).forEach(([k, v]) => {
                        let end = v.get('end')
                        let start = v.get('start')
                        if(end - start < 1000000 && (end > query.start && start < query.end)) {
                            featCallback(v)
                        }
                    })
                } else {
                    for(let i = 0; i < records.length; i++) {
                        featCallback(this._cramRecordToFeature(records[i]))
                    }
                }
                endCallback()
            })
            .catch(err => {
                // map the CramSizeLimitError to JBrowse Errors.DataOverflow
                console.error(err)

                errorCallback(err)
            })
    },

    cleanFeatureCache(query) {
        Object.entries(this.featureCache).forEach(([k, v]) => {
            if((v.get('end') < query.start) || (v.get('start') > query.end)) {
                delete this.featureCache[k]
            }
        })
    },
    getStatsForPairCache() {
        if(Object.keys(this.featureCache).length > 400) {
            var tlens = Object.entries(this.featureCache).map(([k, v]) => Math.abs(v.get('template_length'))).filter(x => x < MAX_INSERT_SIZE_FOR_STATS).sort((a, b) => a - b)
            return {
                upper: Util.percentile(tlens, 0.995),
                lower:  Util.percentile(tlens, 0.005)
            }
        }
        return { upper: Infinity, lower: 0 }
    },

    _cramRecordToFeature(record) {
        return new CramSlightlyLazyFeature(record, this)
    },

    saveStore: function() {
        return {
            urlTemplate: this.config.cram.url,
            craiUrlTemplate: this.config.crai.url
        };
    }

});
});
<commit_msg>Add template length<commit_after>const LRU = cjsRequire('lru-cache')
const { IndexedCramFile, CraiIndex } = cjsRequire('@gmod/cram')

const { Buffer } = cjsRequire('buffer')

const cramIndexedFilesCache = LRU(5)

const BlobFilehandleWrapper = cjsRequire('../../Model/BlobFilehandleWrapper')
const MAX_INSERT_SIZE_FOR_STATS = 30000

class PairedCramRead {
    id() {
        return Math.min(this.read1.id(), this.read2.id())
    }
    get(field) {
        return this._get(field.toLowerCase())
    }
    _get(field) {
        if(field === 'start') {
            return Math.min(this.read1._get('start'), this.read2._get('start'))
        } else if(field === 'end') {
            return Math.max(this.read1._get('end'), this.read2._get('end'))
        } else if(field === 'name') {
            return this.read1._get('name')
        } else if(field === 'pair_orientation') {
            return this.read1._get('pair_orientation')
        } else if(field === 'template_length') {
            return this.read1._get('template_length')
        }
    }
    pairedFeature() { return true }
    children() {}
}
class CramSlightlyLazyFeature {

    _get_name() { return this.record.readName }
    _get_start() { return this.record.alignmentStart-1 }
    _get_end() { return this.record.alignmentStart+this.record.lengthOnRef-1 }
    _get_cram_read_features() { return this.record.readFeatures }
    _get_type() { return 'match'}
    _get_mapping_quality() { return this.record.mappingQuality}
    _get_flags() { return `0x${this.record.flags.toString(16)}`}
    _get_cramFlags() { return `0x${this.record.cramFlags.toString(16)}`}
    _get_strand() { return this.record.isReverseComplemented() ? -1 : 1 }
    _get_read_group_id() { return this.record.readGroupId }
    _get_qual() { return (this.record.qualityScores || []).map(q => q+33).join(' ')}
    _get_seq() { return this.record.readBases}
    _get_seq_id() { return this._store._refIdToName(this.record.sequenceId)}
    _get_qc_failed() { return this.record.isFailedQc()}
    _get_secondary_alignment() { return this.record.isSecondary()}
    _get_duplicate() { return this.record.isDuplicate()}
    _get_supplementary_alignment() { return this.record.isSupplementary()}
    _get_pair_orientation() { return this.record.getPairOrientation()}
    _get_multi_segment_template() { return this.record.isPaired()}
    _get_multi_segment_all_correctly_aligned() { return this.record.isProperlyPaired()}
    _get_multi_segment_all_aligned() { return this.record.isProperlyPaired()}
    _get_multi_segment_next_segment_unmapped() { return this.record.isMateUnmapped()}
    _get_multi_segment_first() { return this.record.isRead1()}
    _get_multi_segment_last() { return this.record.isRead2()}
    _get_multi_segment_next_segment_reversed() { return this.record.isMateReverseComplemented()}
    _get_unmapped() { return this.record.isSegmentUnmapped()}
    _get_template_length() { return this.record.templateLength}
    _get_next_seq_id() { return this.record.mate ? this._store._refIdToName(this.record.mate.sequenceId) : undefined }
    _get_next_segment_position() { return this.record.mate
        ? ( this._store._refIdToName(this.record.mate.sequenceId)+':'+this.record.mate.alignmentStart) : undefined}
    _get_tags() { return this.record.tags }
    _get_seq() { return this.record.getReadBases() }

    constructor(record, store) {
        this.record = record
        this._store = store
    }

    tags() {
        const properties = Object.getOwnPropertyNames(CramSlightlyLazyFeature.prototype)
        return properties
            .filter(prop => /^_get_/.test(prop))
            .map(methodName => methodName.replace('_get_',''))
    }

    id() {
        return this.record.uniqueId + 1
    }

    _get(field) {
        const methodName = `_get_${field}`
        if (this[methodName]) return this[methodName]()
        return undefined
    }
    get(field) {
        const methodName = `_get_${field.toLowerCase()}`
        if (this[methodName]) return this[methodName]()
        return undefined
    }

    parent() {}

    children() {}

    pairedFeature() { return false }
}

function canBePaired(alignment) {
    return alignment.isPaired() &&
        !alignment.isMateUnmapped() &&
        alignment.sequenceId === alignment.mate.sequenceId &&
        (alignment.isRead1() || alignment.isRead2()) &&
        !(alignment.isSecondary() || alignment.isSupplementary());
}

define( [
            'dojo/_base/declare',
            'JBrowse/Util',
            'JBrowse/Errors',
            'JBrowse/Store/SeqFeature',
            'JBrowse/Store/DeferredStatsMixin',
            'JBrowse/Store/DeferredFeaturesMixin',
            'JBrowse/Store/SeqFeature/GlobalStatsEstimationMixin',
            'JBrowse/Model/XHRBlob',
            'JBrowse/Model/SimpleFeature',
        ],
        function(
            declare,
            Util,
            Errors,
            SeqFeatureStore,
            DeferredStatsMixin,
            DeferredFeaturesMixin,
            GlobalStatsEstimationMixin,
            XHRBlob,
            SimpleFeature,
        ) {

return declare( [ SeqFeatureStore, DeferredStatsMixin, DeferredFeaturesMixin, GlobalStatsEstimationMixin ],

/**
 * @lends JBrowse.Store.SeqFeature.CRAM
 */
{
    /**
     * Data backend for reading feature data directly from a
     * web-accessible CRAM file.
     *
     * @constructs
     */
    constructor: function( args ) {

        let dataBlob
        if (args.cram)
            dataBlob = new BlobFilehandleWrapper(args.cram)
        else if (args.urlTemplate)
            dataBlob = new BlobFilehandleWrapper(new XHRBlob(this.resolveUrl(args.urlTemplate || 'data.cram'), { expectRanges: true }))
        else throw new Error('must provide either `cram` or `urlTemplate`')

        let indexBlob
        if (args.crai)
            indexBlob = new BlobFilehandleWrapper(args.crai)
        else if (args.craiUrlTemplate)
            indexBlob = new BlobFilehandleWrapper(new XHRBlob(this.resolveUrl(args.craiUrlTemplate)))
        else if (args.urlTemplate)
            indexBlob = new BlobFilehandleWrapper(new XHRBlob(this.resolveUrl(args.urlTemplate+'.crai')))
        else throw new Error('no index provided, must provide a CRAM index')

        this.source = dataBlob.toString()

        // LRU-cache the CRAM object so we don't have to re-download the
        // index when we switch chromosomes
        const cacheKey = `data: ${dataBlob}, index: ${indexBlob}`
        this.cram = cramIndexedFilesCache.get(cacheKey)
        if (!this.cram) {
            this.cram = new IndexedCramFile({
                cramFilehandle: dataBlob,
                index: new CraiIndex({filehandle: indexBlob}),
                seqFetch: this._seqFetch.bind(this),
                checkSequenceMD5: false,
            })

            cramIndexedFilesCache.set(cacheKey, this.cram)
        }

        // pre-download the index before running the statistics estimation so that the stats
        // estimation doesn't time out
        this.cram.hasDataForReferenceSequence(0)
            .then(() => this.cram.cram.getSamHeader())
            .then(samHeader => {
                this._setSamHeader(samHeader)
            })
            .then(() => {
                this._deferred.features.resolve({success:true});
            })
            .then(() => this._estimateGlobalStats())
            .then(stats => {
                this.globalStats = stats;
                this._deferred.stats.resolve({success:true});
            })
            .catch(err => {
                this._deferred.features.reject(err)
                this._deferred.stats.reject(err)
            })

        this.storeTimeout = args.storeTimeout || 3000;
        this.featureCache = {}
    },

    // process the parsed SAM header from the cram file
    _setSamHeader(samHeader) {
        this._samHeader = {}

        // use the @SQ lines in the header to figure out the
        // mapping between ref seq ID numbers and names
        const refSeqIdToName = []
        const refSeqNameToId = {}
        const sqLines = samHeader.filter(l => l.tag === 'SQ')
        sqLines.forEach((sqLine, seqId) => {
            sqLine.data.forEach(item => {
                if (item.tag === 'SN') {
                    // this is the seq name
                    const seqName = item.value
                    refSeqNameToId[seqName] = seqId
                    refSeqIdToName[seqId] = seqName
                }
            })
        })
        if (refSeqIdToName.length) {
            this._samHeader.refSeqIdToName = refSeqIdToName
            this._samHeader.refSeqNameToId = refSeqNameToId
        }
    },

    _refNameToId(refName) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqNameToId)
            return this._samHeader.refSeqNameToId[refName]
        else
            return this.browser.getRefSeqNumber(refName)
    },

    _refIdToName(refId) {
        // use info from the SAM header if possible, but fall back to using
        // the ref seq order from when the browser's refseqs were loaded
        if (this._samHeader.refSeqIdToName) {
            return this._samHeader.refSeqIdToName[refId]
        } else {
            let ref = this.browser.getRefSeqById(refId)
            return ref ? ref.name : undefined
        }
    },

    _getRefSeqStore() {
        return new Promise((resolve,reject) => {
            this.browser.getStore('refseqs',resolve,reject)
        })
    },

    // used by the CRAM backend to fetch a region of the underlying reference
    // sequence.  needed for some of its calculations
    async _seqFetch(seqId, start, end) {
        start -= 1 // convert from 1-based closed to interbase

        const refSeqStore = await this._getRefSeqStore()
        if (!refSeqStore) return undefined
        const refName = this._refIdToName(seqId)
        if (!refName) return undefined

        const seqChunks = await new Promise((resolve,reject) => {
            let features = []
            refSeqStore.getFeatures(
                {ref: refName, start: start-1, end},
                f => features.push(f),
                () => resolve(features),
                reject
            )
        })

        const trimmed = []
        seqChunks
        .sort((a,b) => a.get('start') - b.get('start'))
        .forEach( (chunk,i) => {
            let chunkStart = chunk.get('start')
            let chunkEnd = chunk.get('end')
            let trimStart = Math.max(start - chunkStart, 0)
            let trimEnd = Math.min(end - chunkStart, chunkEnd-chunkStart)
            let trimLength = trimEnd - trimStart
            let chunkSeq = chunk.get('seq') || chunk.get('residues')
            trimmed.push(chunkSeq.substr(trimStart,trimLength))
        })

        const sequence = trimmed.join('')
        if (sequence.length !== (end-start))
            throw new Error(`sequence fetch failed: fetching ${
                (start-1).toLocaleString()}-${end.toLocaleString()
                } only returned ${
                    sequence.length.toLocaleString()
                } bases, but should have returned ${
                    (end-start).toLocaleString()
                }`)
        return sequence
    },

    /**
     * Interrogate whether a store has data for a given reference
     * sequence.  Calls the given callback with either true or false.
     */
    hasRefSeq: function( seqName, callback, errorCallback ) {
        seqName = this.browser.regularizeReferenceName( seqName );
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) callback(false)

        this._deferred.stats
        .then(() => this.cram.hasDataForReferenceSequence(refSeqNumber))
        .then(callback, errorCallback)
    },

    // called by getFeatures from the DeferredFeaturesMixin
    _getFeatures: function( query, featCallback, endCallback, errorCallback ) {
        const pairCache = {};
        const seqName = query.ref || this.refSeq.name
        const refSeqNumber = this._refNameToId(seqName)
        if (refSeqNumber === undefined) {
            endCallback()
            return
        }
        this.cram.getRecordsForRange(refSeqNumber, query.start + 1, query.end, {viewAsPairs: query.viewAsPairs})
            .then(records => {
                if(query.viewAsPairs) {
                    records.sort((a, b) => {
                        return (a.readName < b.readName ? -1 : (a.readName > b.readName ? 1 : 0));
                    })
                    for(let i = 0; i < records.length; i++) {
                        let feat
                        if (canBePaired(records[i])) {
                            let name = records[i].readName
                            feat = pairCache[name]
                            if (feat) {
                                if(records[i].isRead1()) {
                                    feat.read1 = this._cramRecordToFeature(records[i])
                                } else if(records[i].isRead2()) {
                                    feat.read2 = this._cramRecordToFeature(records[i])
                                } else {
                                    console.log('unable to pair read',records[i])
                                }
                                if(feat.read1 && feat.read2) {
                                    delete pairCache[name]
                                    this.featureCache[name] = feat
                                }
                            }
                            else {
                                feat = new PairedCramRead()
                                if(records[i].isRead1()) {
                                    feat.read1 = this._cramRecordToFeature(records[i])
                                } else if(records[i].isRead2()) {
                                    feat.read2 = this._cramRecordToFeature(records[i])
                                } else {
                                    console.log('unable to pair read', records[i])
                                }
                                pairCache[name] = feat
                            }
                        }
                        else if(!((records[i].alignmentStart+records[i].lengthOnRef-1) < query.start) && !(records[i].alignmentStart > query.end)){
                            let feat = this._cramRecordToFeature(records[i])
                            featCallback(feat)
                        }
                    }
                    Object.entries(this.featureCache).forEach(([k, v]) => {
                        let end = v.get('end')
                        let start = v.get('start')
                        if(end - start < 1000000 && (end > query.start && start < query.end)) {
                            featCallback(v)
                        }
                    })
                } else {
                    for(let i = 0; i < records.length; i++) {
                        featCallback(this._cramRecordToFeature(records[i]))
                    }
                }
                endCallback()
            })
            .catch(err => {
                // map the CramSizeLimitError to JBrowse Errors.DataOverflow
                console.error(err)

                errorCallback(err)
            })
    },

    cleanFeatureCache(query) {
        Object.entries(this.featureCache).forEach(([k, v]) => {
            if((v.get('end') < query.start) || (v.get('start') > query.end)) {
                delete this.featureCache[k]
            }
        })
    },
    getStatsForPairCache() {
        if(Object.keys(this.featureCache).length > 400) {
            var tlens = Object.entries(this.featureCache).map(([k, v]) => Math.abs(v.get('template_length'))).filter(x => x < MAX_INSERT_SIZE_FOR_STATS).sort((a, b) => a - b)
            return {
                upper: Util.percentile(tlens, 0.995),
                lower:  Util.percentile(tlens, 0.005)
            }
        }
        return { upper: Infinity, lower: 0 }
    },

    _cramRecordToFeature(record) {
        return new CramSlightlyLazyFeature(record, this)
    },

    saveStore: function() {
        return {
            urlTemplate: this.config.cram.url,
            craiUrlTemplate: this.config.crai.url
        };
    }

});
});
<|endoftext|>