Repository URL to install this package:
Version:
2.0.11-7 ▾
|
"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