Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
notion-desktop / usr / lib / notion-desktop / resources / app / shared / AssetCache.js
Size: Mime:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const urlHelpers = require("./urlHelpers");
const path = require("path");
const EventEmitterMap_1 = require("./EventEmitterMap");
const AsyncQueue_1 = require("./AsyncQueue");
const mathUtils_1 = require("./mathUtils");
class AssetCache {
    constructor(args) {
        this.args = args;
        this.queue = new AsyncQueue_1.AsyncQueue(1);
        this.events = new EventEmitterMap_1.default();
        this.appActive = true;
        this.lastAppStateChangeTime = 0;
        this.latestVersionFileName = "latestVersion.json";
        this.assetsJsonFileName = "assets.json";
        this.assetHeadersFileName = "headers.json";
        this.assetsDirName = "assets";
        this.assetCacheDirName = "notionAssetCache-v2";
        this.cacheDir = path.join(this.args.baseDir, this.assetCacheDirName);
        this.latestVersionPath = path.join(this.cacheDir, this.latestVersionFileName);
    }
    async handleRequest(req) {
        const urlPath = urlHelpers.parse(req.url).pathname || "/";
        const { logger } = this.args;
        if (!this.assetCacheState) {
            return;
        }
        let assetCacheState = this.assetCacheState;
        if (assetCacheState.assetsJson.proxyServerPathPrefixes.some(prefix => urlPath.startsWith(prefix))) {
            return;
        }
        const assetFile = assetCacheState.assetsJson.files.find(file => file.path === urlPath);
        if (assetFile) {
            const currentAssetsDir = this.getAssetsDir(assetCacheState.assetsJson.version);
            const absolutePath = path.join(currentAssetsDir, assetFile.path);
            logger.info(`Performing file request: ${urlPath}, abs path ${absolutePath}`);
            return {
                absolutePath: absolutePath,
                headers: this.getHeaders(assetFile.path),
            };
        }
        await this.syncVersions();
        assetCacheState = this.assetCacheState;
        if (assetCacheState) {
            const currentAssetsDir = this.getAssetsDir(assetCacheState.assetsJson.version);
            const indexAssetFile = assetCacheState.assetsJson.files.find(file => file.path === assetCacheState.assetsJson.entry);
            if (indexAssetFile) {
                if (urlPath.includes(".")) {
                    this.args.loggly.rateLimitedLog({
                        level: "error",
                        from: "AssetCache",
                        type: "requestReturnedAsIndexV2",
                        error: { urlPath },
                    });
                }
                const absolutePath = path.join(currentAssetsDir, indexAssetFile.path);
                logger.info(`Performing file request: ${urlPath}, abs path ${absolutePath}`);
                return {
                    absolutePath: absolutePath,
                    headers: this.getHeaders(indexAssetFile.path),
                };
            }
        }
        this.args.loggly.rateLimitedLog({
            level: "error",
            from: "AssetCache",
            type: "cannotFindIndex",
            error: { urlPath },
        });
        return;
    }
    initialize() {
        if (this.ready) {
            return this.ready;
        }
        this.ready = (async () => {
            const { logger } = this.args;
            logger.info(`latestVersion.json path ${this.latestVersionPath}`);
            this.latestVersion = await this.loadJson(this.latestVersionPath);
            logger.info(`Current version loaded: ${JSON.stringify(this.latestVersion)}`);
            await this.syncVersions();
            logger.info(`Current synced assets.json: ${this.assetCacheState && this.assetCacheState.assetsJson.version}`);
            await this.cleanOldVersions();
        })();
        return this.ready;
    }
    async reset() {
        this.queue.enqueue(async () => {
            this.assetCacheState = undefined;
            this.latestVersion = undefined;
            await this.cleanOldVersions();
            await this.checkForUpdatesNow();
        });
    }
    checkForUpdates() {
        return this.queue.enqueue(() => this.checkForUpdatesNow());
    }
    async checkForUpdatesNow() {
        const { logger, fs } = this.args;
        const checkForUpdatesNowStart = Date.now();
        logger.info("Checking for app update");
        this.events.emit("checking-for-update");
        const updateAssetsFetchStart = Date.now();
        let response;
        try {
            response = await fetch(urlHelpers.resolve(this.args.baseUrl, "/api/v3/getAssetsJson"), { method: "post", body: "{}" });
        }
        catch (error) {
            logger.info("No app update available");
            this.events.emit("update-not-available");
            return;
        }
        if (response.status !== 200) {
            const error = new Error(response.status + ": " + response.statusText);
            this.args.loggly.log({
                level: "error",
                from: "assetCache",
                type: "Non200Response",
                error: error,
                data: {
                    ...this.createErrorDataMetrics(updateAssetsFetchStart),
                },
            });
            this.events.emit("error", error);
            return;
        }
        this.logPerformance("updateAssetsFetch", updateAssetsFetchStart);
        const updateAssetsResponseParseStart = Date.now();
        let newAssetsJson;
        try {
            newAssetsJson = await response.json();
        }
        catch (error) {
            this.args.loggly.log({
                level: "error",
                from: "assetCache",
                type: "parseError",
                error: error,
                data: {
                    ...this.createErrorDataMetrics(updateAssetsResponseParseStart),
                },
            });
            this.events.emit("error", error);
            return;
        }
        this.logPerformance("updateAssetsResponseParse", updateAssetsResponseParseStart);
        const assetJsonStart = Date.now();
        const updateAvailable = !this.latestVersion ||
            this.latestVersion.version !== newAssetsJson.version;
        if (!updateAvailable) {
            logger.info("No app update available");
            this.events.emit("update-not-available");
            return;
        }
        logger.info("App update available", newAssetsJson.version);
        this.events.emit("update-available", newAssetsJson);
        const newAssetHeaders = {};
        const newCacheDir = this.getCacheDir(newAssetsJson.version);
        const newAssetsDir = this.getAssetsDir(newAssetsJson.version);
        const newAssetsJsonPath = this.getAssetsJsonPath(newAssetsJson.version);
        const newAssetHeadersPath = this.getAssetHeadersPath(newAssetsJson.version);
        const newCacheDirExists = await this.directoryExists(newCacheDir);
        const copiedFilePaths = new Set();
        if (!newCacheDirExists) {
            try {
                await fs.mkdirp(newCacheDir);
            }
            catch (error) {
                this.args.loggly.log({
                    level: "error",
                    from: "assetCache",
                    type: "mkdirpError",
                    error: error,
                    data: {
                        ...this.createErrorDataMetrics(assetJsonStart),
                    },
                });
                this.events.emit("error", error);
                return;
            }
            if (this.assetCacheState) {
                const assetCacheState = this.assetCacheState;
                const currentAssetsJson = assetCacheState.assetsJson;
                const currentAssetHeaders = assetCacheState.assetHeaders;
                const currentAssetsSet = new Set(currentAssetsJson.files.map(assetFile => assetFile.path));
                const filesWithSameFilePaths = newAssetsJson.files.filter(file => currentAssetsSet.has(file.path));
                const currentCacheDir = this.getCacheDir(currentAssetsJson.version);
                const currentAssetsDir = path.join(currentCacheDir, this.assetsDirName);
                for (const file of filesWithSameFilePaths) {
                    const matchedPath = file.path;
                    const currentAssetPath = path.join(currentAssetsDir, matchedPath);
                    const newAssetPath = path.join(newAssetsDir, matchedPath);
                    newAssetHeaders[matchedPath] = currentAssetHeaders[matchedPath];
                    try {
                        await fs.copy({ src: currentAssetPath, dest: newAssetPath });
                        copiedFilePaths.add(matchedPath);
                    }
                    catch (error) {
                        this.args.loggly.log({
                            level: "error",
                            from: "assetCache",
                            type: "mkdirpError",
                            error: error,
                            data: {
                                ...this.createErrorDataMetrics(assetJsonStart),
                            },
                        });
                    }
                }
            }
        }
        let downloaded = 0;
        const emit = () => {
            this.events.emit("download-progress", {
                downloaded: downloaded,
                total: newAssetsJson.files.length,
            });
        };
        emit();
        this.logPerformance("assetJson", assetJsonStart);
        const prepareStart = Date.now();
        const queue = new AsyncQueue_1.AsyncQueue(8);
        const errors = [];
        await Promise.all(newAssetsJson.files.map(file => {
            return queue.enqueue(async () => {
                if (copiedFilePaths.has(file.path) &&
                    (await this.verifyAsset(newAssetsDir, file))) {
                    downloaded++;
                    emit();
                    return;
                }
                const newAssetPath = path.join(newAssetsDir, file.path);
                try {
                    const headers = await this.args.fs.downloadFile({
                        url: urlHelpers.resolve(this.args.baseUrl, file.path),
                        dest: newAssetPath,
                    });
                    newAssetHeaders[file.path] = headers;
                    const newAssetIsValid = await this.verifyAsset(newAssetsDir, file);
                    if (newAssetIsValid) {
                        downloaded++;
                        emit();
                    }
                    else {
                        const error = new Error("Invalid asset hash");
                        error["data"] = { filePath: file.path };
                        errors.push(error);
                    }
                }
                catch (err) {
                    err["data"] = { filePath: file.path };
                    errors.push(err);
                }
            });
        }));
        this.logPerformance("prepare", prepareStart);
        const downloadStart = Date.now();
        if (errors.length > 0) {
            this.args.loggly.log({
                level: "error",
                from: "assetCache",
                type: "downloadError",
                data: {
                    errors: errors,
                    ...this.createErrorDataMetrics(assetJsonStart),
                },
            });
            this.events.emit("error", errors[0]);
            return;
        }
        const headersWriteSuccessful = await this.writeJson(newAssetHeadersPath, newAssetHeaders);
        if (!headersWriteSuccessful) {
            this.events.emit("error", new Error("Cannot write headers.json"));
            return;
        }
        const assetsJsonWriteSuccessful = await this.writeJson(newAssetsJsonPath, newAssetsJson);
        if (!assetsJsonWriteSuccessful) {
            this.events.emit("error", new Error("Cannot write assets.json"));
            return;
        }
        const newLatestVersion = {
            version: newAssetsJson.version,
        };
        const latestVersionWriteSuccessful = await this.writeJson(this.latestVersionPath, newLatestVersion);
        if (!latestVersionWriteSuccessful) {
            this.events.emit("error", new Error("Cannot write latestVersion.json"));
            return;
        }
        this.latestVersion = newLatestVersion;
        this.args.logger.info("App update download complete", newAssetsJson.version);
        this.events.emit("update-downloaded", newAssetsJson);
        this.args.logger.info("Installing app update", newAssetsJson.version);
        this.events.emit("update-finished", newAssetsJson);
        this.logPerformance("download", downloadStart);
        this.logPerformance("checkForUpdatesNow", checkForUpdatesNowStart);
    }
    updateAppState(appActive, lastAppStateChangeTime) {
        this.appActive = appActive;
        this.lastAppStateChangeTime = lastAppStateChangeTime;
    }
    logPerformance(type, start) {
        const end = Date.now();
        if (!this.appActive || start < this.lastAppStateChangeTime) {
            return;
        }
        if (mathUtils_1.randomlySucceedWithPercentage(1)) {
            this.args.loggly.log({
                level: "info",
                from: "assetCache",
                type: type,
                data: {
                    time: end - start,
                },
            });
        }
    }
    createErrorDataMetrics(start) {
        const end = Date.now();
        if (!this.appActive || start < this.lastAppStateChangeTime) {
            return {};
        }
        return {
            time: end - start,
        };
    }
    async syncVersions() {
        const { logger } = this.args;
        if (!this.latestVersion) {
            logger.info("Sync versions: empty latestVersion");
            return;
        }
        if (this.assetCacheState &&
            this.latestVersion.version === this.assetCacheState.assetsJson.version) {
            logger.info("Sync versions: same version skipping sync");
            return;
        }
        const assetsJsonPath = this.getAssetsJsonPath(this.latestVersion.version);
        const headersJsonPath = this.getAssetHeadersPath(this.latestVersion.version);
        logger.info(`Sync versions: assets.json path ${assetsJsonPath} headers.json path ${headersJsonPath}`);
        const assetsJson = await this.loadJson(assetsJsonPath);
        const assetHeaders = await this.loadJson(headersJsonPath);
        if (assetsJson && assetHeaders) {
            this.assetCacheState = { assetsJson, assetHeaders };
        }
    }
    async cleanOldVersions() {
        let subpathsToDelete = await this.readDir(this.cacheDir);
        if (this.assetCacheState && this.latestVersion) {
            const assetCacheState = this.assetCacheState;
            const latestVersion = this.latestVersion.version;
            subpathsToDelete = subpathsToDelete.filter(subpath => subpath !== this.latestVersionFileName &&
                subpath !== assetCacheState.assetsJson.version &&
                subpath !== latestVersion);
        }
        await Promise.all(subpathsToDelete.map(async (subpath) => this.remove(this.getCacheDir(subpath))));
    }
    async verifyAsset(assetDir, file) {
        const filePath = path.join(assetDir, file.path);
        const hash = await this.getFileHash(filePath);
        if (hash !== file.hash) {
            return false;
        }
        return true;
    }
    getHeaders(assetSubpath) {
        const assetCacheState = this.assetCacheState;
        if (!assetCacheState) {
            return {};
        }
        const headers = assetCacheState.assetHeaders[assetSubpath];
        if (!headers) {
            return {};
        }
        const lowerCaseHeaders = {};
        for (const key in headers) {
            lowerCaseHeaders[key.toLowerCase()] = headers[key];
        }
        const filteredHeaders = {};
        const headersWhitelist = assetCacheState.assetsJson.headersWhitelist;
        for (const key of headersWhitelist) {
            const lowerCaseKey = key.toLowerCase();
            if (lowerCaseHeaders[lowerCaseKey]) {
                filteredHeaders[lowerCaseKey] = lowerCaseHeaders[lowerCaseKey];
            }
        }
        return filteredHeaders;
    }
    async loadJson(absolutePath) {
        try {
            const contents = await this.args.fs.readFile(absolutePath);
            return JSON.parse(contents);
        }
        catch (error) {
            this.args.logger.info("Error reading " + absolutePath, error);
        }
    }
    async writeJson(absolutePath, contents) {
        try {
            if (contents === undefined) {
                await this.args.fs.remove(absolutePath);
            }
            else {
                await this.args.fs.writeFile(absolutePath, JSON.stringify(contents));
            }
            return true;
        }
        catch (error) {
            this.args.loggly.log({
                level: "error",
                from: "AssetCache",
                type: "failedToWriteFile",
                error: { absolutePath, error },
            });
            return false;
        }
    }
    getCacheDir(subpath) {
        return path.join(this.cacheDir, subpath);
    }
    getAssetsDir(version) {
        return path.join(this.getCacheDir(version), this.assetsDirName);
    }
    getAssetsJsonPath(version) {
        return path.join(this.getCacheDir(version), this.assetsJsonFileName);
    }
    getAssetHeadersPath(version) {
        return path.join(this.getCacheDir(version), this.assetHeadersFileName);
    }
    async directoryExists(dir) {
        try {
            return await this.args.fs.isDirectory(dir);
        }
        catch (error) {
            return false;
        }
    }
    async readDir(dirPath) {
        try {
            const results = await this.args.fs.readdir(dirPath);
            return results;
        }
        catch (error) {
            return [];
        }
    }
    async remove(dirOrFilePath) {
        try {
            await this.args.fs.remove(dirOrFilePath);
        }
        catch (error) { }
    }
    async getFileHash(filePath) {
        try {
            const hash = await this.args.fs.getFileHash(filePath);
            return hash;
        }
        catch (error) { }
    }
}
exports.AssetCache = AssetCache;
//# sourceMappingURL=AssetCache.js.map