UNPKG

@localazy/cdn-client

Version:

Node.js module that allows you to easily interact with the Localazy CDN.

643 lines (642 loc) 17.6 kB
class Api { context; constructor(context) { this.context = context; } async fetchLocale(options) { if (this.context.cache.has(options)) { return new Promise((resolve) => { resolve(this.context.cache.get(options)); }); } return this.context.client.get(options.metafileLocale.uri); } async fetchMetafile() { return await this.context.client.get(this.context.metafile.params.jsonPath); } } class MemoryCacheAdapter { map; constructor() { this.map = /* @__PURE__ */ new Map(); } get(key) { return this.map.get(key); } has(key) { return this.map.has(key); } set(key, value) { this.map.set(key, value); } flush() { this.map = /* @__PURE__ */ new Map(); } } const isString = (value) => typeof value === "string"; const isUndefined = (value) => typeof value === "undefined"; const isArray = (value) => Array.isArray(value); const isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]"; const uniq = (array) => [...new Set(array)]; const uniqBy = (array, predicate) => { const seen = {}; return array.filter((item) => { const key = predicate(item); if (!Object.hasOwn(seen, key)) { seen[key] = true; return true; } return false; }); }; class LocalesCache { context; cacheAdapter; constructor(context) { this.context = context; this.cacheAdapter = new MemoryCacheAdapter(); } setIfMissed(options) { const { metafileFile, metafileLocale, data } = options; const key = this.keyFromMetafile({ metafileFile, metafileLocale }); if (!this.cacheAdapter.has(key)) { this.cacheAdapter.set(key, data); } } has(options) { const key = this.keyFromMetafile(options); return this.cacheAdapter.has(key); } get(options) { const key = this.keyFromMetafile(options); return this.cacheAdapter.get(key); } flush() { this.cacheAdapter.flush(); } keyFromMetafile(options) { const { metafileFile, metafileLocale } = options; const productFlavors = [...uniq(metafileFile.productFlavors)].sort().join("-"); const indices = [ this.context.metafile.params.cdnId, metafileFile.id, metafileFile.file, metafileFile.path, metafileFile.library, metafileFile.module, metafileFile.buildType, productFlavors, metafileLocale.locale, metafileLocale.timestamp.toString() ]; return indices.filter((key) => key !== "").join("-"); } } class ResponseFactory { context; constructor(context) { this.context = context; } createCdnResponse(options) { const { requests, responses, hasSingleFileResponse, hasSingleLocaleResponse } = options; if (responses.length === 0 || typeof responses[0] === "undefined") { return {}; } this.cacheResponses(requests, responses); return hasSingleFileResponse && hasSingleLocaleResponse ? responses[0] : ResponseFactory.transformResponses(options); } cacheResponses(requests, responses) { responses.forEach((response, index) => { if (typeof requests[index] !== "undefined") { const { metafileFile, metafileLocale } = requests[index]; if (metafileFile && metafileLocale) { this.context.cache.setIfMissed({ metafileFile, metafileLocale, data: response }); } } }); } static transformResponses(options) { const { requests, responses, hasSingleFileResponse } = options; return responses.reduce((acc, cur, index) => { if (typeof requests[index] !== "undefined") { const { metafileFile, metafileLocale } = requests[index]; if (metafileFile && metafileLocale) { if (hasSingleFileResponse) { acc[metafileLocale.locale] = cur; } else { if (!acc[metafileFile.id]) { acc[metafileFile.id] = {}; } acc[metafileFile.id][metafileLocale.locale] = cur; } } } return acc; }, {}); } } class Context { metafile; cdn; client; api; cache; responseFactory; constructor(options) { this.metafile = options.metafileContext; this.cdn = options.cdn; this.client = options.client; this.api = new Api(this); this.cache = new LocalesCache(this); this.responseFactory = new ResponseFactory(this); } } class MetafileFile { id; file; path; library; module; buildType; timestamp; productFlavors; locales; baseUrl; constructor(options) { this.id = options.id; this.file = options.file; this.path = options.path; this.library = options.library; this.module = options.module; this.buildType = options.buildType; this.timestamp = options.timestamp; this.productFlavors = options.productFlavors; this.locales = options.locales; this.baseUrl = options.baseUrl; } toCdnFile() { return { id: this.id, file: this.file, path: this.path, library: this.library, module: this.module, buildType: this.buildType, productFlavors: this.productFlavors, locales: this.locales.map( (locale) => ({ locale: locale.locale, isBaseLocale: locale.isBaseLocale, uri: `${this.baseUrl}${locale.uri}` }) ) }; } } class MetafileLocale { language; region; script; isRtl; name; localizedName; uri; timestamp; baseLocale; constructor(options, baseLocale) { this.language = options.language; this.region = options.region; this.script = options.script; this.isRtl = options.isRtl; this.name = options.name; this.localizedName = options.localizedName; this.uri = options.uri; this.timestamp = options.timestamp; this.baseLocale = baseLocale; } get locale() { if (this.language && this.region && this.script) { return `${this.language}_${this.region}#${this.script}`; } if (this.language && this.script) { return `${this.language}#${this.script}`; } if (this.language && this.region) { return `${this.language}_${this.region}`; } return this.language; } get isBaseLocale() { return this.locale === this.baseLocale; } toCdnLocale() { return { locale: this.locale, isBaseLocale: this.isBaseLocale, language: this.language, region: this.region, script: this.script, isRtl: this.isRtl, name: this.name, localizedName: this.localizedName }; } } class MetafileData { projectUrl; baseLocale; locales; timestamp; files; filesMap; constructor(options, params) { this.projectUrl = options.projectUrl; this.timestamp = options.timestamp; this.files = MetafileData.filesFactory(options.files, options.baseLocale, params); this.filesMap = MetafileData.filesMapFactory(this.files); this.locales = MetafileData.localesFactory(this.files); this.baseLocale = this.locales.find((locale) => locale.isBaseLocale); } static createEmpty(params) { return new MetafileData( { projectUrl: "", baseLocale: "", timestamp: 0, files: {} }, params ); } static filesFactory(files, baseLocale, params) { return Object.keys(files).reduce((acc, cur) => { if (typeof files[cur] !== "undefined") { const locales = files[cur].locales.map( (locale) => new MetafileLocale(locale, baseLocale) ); acc.push( new MetafileFile({ ...files[cur], id: cur, locales, baseUrl: params.baseUrl }) ); } return acc; }, []); } static filesMapFactory(files) { return files.reduce((acc, cur) => { acc[cur.id] = cur; return acc; }, {}); } static localesFactory(files) { const locales = files.reduce((acc, cur) => { acc.push(...cur.locales.map((locale) => locale.toCdnLocale())); return acc; }, []); return uniqBy(locales, (cdnLocale) => cdnLocale.locale); } } class MetafileParams { options; constructor(metafileUrl) { this.options = MetafileParams.parseMetafileUrl(metafileUrl); } get url() { return this.options.url; } get baseUrl() { return this.options.baseUrl; } get cdnId() { return this.options.cdnId; } get jsonPath() { return this.options.jsonPath; } static parseMetafileUrl(metafileUrl) { let url; try { url = new URL(metafileUrl); } catch { throw new Error('Invalid param: "options.metafile" cannot be parsed as url.'); } const matches = /^\/(.*?)\/(.*?)\.(v2.json|js|ts)$/.exec(url.pathname); if (matches === null || matches.length !== 4 || typeof matches[1] === "undefined" || typeof matches[2] === "undefined") { throw new Error('Invalid param: "options.metafile" contains invalid metafile url.'); } const cdnId = matches[1]; const tagId = matches[2]; return { url: metafileUrl, baseUrl: url.origin, cdnId, jsonPath: `/${cdnId}/${tagId}.v2.json` }; } } class MetafileContext { params; parsedData; data; constructor(options) { this.params = new MetafileParams(options.metafile); this.parsedData = null; this.data = MetafileData.createEmpty(this.params); } setMetafile(metafile) { this.parsedData = metafile; this.data = new MetafileData(metafile, this.params); } } class FetchHttpAdapter { baseUrl; constructor(baseUrl) { this.baseUrl = baseUrl; } async get(url) { const response = await fetch(`${this.baseUrl}${url}`); const contentType = response.headers.get("content-type"); const isJson = contentType === "application/json5" || contentType === "application/json"; if (response.status >= 400) { throw new Error(`Request failed with status code ${response.status.toString()}`); } let result; if (isJson) { result = await response.json(); } else { result = await response.text(); } return result; } } class CdnBase { context; constructor(context) { this.context = context; } } class CdnCache extends CdnBase { flush = () => { this.context.cache.flush(); }; } class CdnMetafile extends CdnBase { get projectUrl() { return this.context.metafile.data.projectUrl; } get baseLocale() { return this.context.metafile.data.baseLocale; } get url() { return this.context.metafile.params.url; } get files() { return this.context.metafile.data.files.map((file) => file.toCdnFile()); } locales = (options) => { const { excludeBaseLocale } = options || {}; const { locales } = this.context.metafile.data; return excludeBaseLocale ? locales.filter((cdnLocale) => !cdnLocale.isBaseLocale) : locales; }; refresh = async () => { const response = await this.context.api.fetchMetafile(); this.context.metafile.setMetafile(response); }; switch = async (options) => { this.context.metafile.params = new MetafileParams(options.metafile); await this.refresh(); }; } class LocalesMap { data; context; constructor(options) { this.context = options.context; this.data = options.data || {}; } } class Request { files; localesMap; hasSingleFileResponse; hasSingleLocaleResponse; context; constructor(context) { this.files = []; this.localesMap = new LocalesMap({ context }); this.hasSingleFileResponse = false; this.hasSingleLocaleResponse = false; this.context = context; } async execute() { const payload = this.getPromises(); const promises = payload.map( (item) => item[0] ); const requests = payload.map( (item) => item[1] ); const responses = await Promise.all(promises); return this.context.responseFactory.createCdnResponse({ requests, responses, localesMap: this.localesMap, hasSingleFileResponse: this.hasSingleFileResponse, hasSingleLocaleResponse: this.hasSingleLocaleResponse }); } getPromises() { return this.files.reduce( (acc, cur) => { if (typeof this.localesMap.data?.[cur.id] !== "undefined") { acc.push( ...this.localesMap.data[cur.id].map( (metafileLocale) => { const request = { metafileFile: cur, metafileLocale }; return [this.context.api.fetchLocale(request), request]; } ) ); } return acc; }, [] ); } } class RequestBuilder { context; request; constructor(context) { this.context = context; this.request = new Request(this.context); } addFiles(files) { if (!(isPlainObject(files) || isString(files) || isUndefined(files) || isArray(files))) { throw new Error('Invalid param: "request.files" must be object, array, string or undefined.'); } if (isArray(files)) { files.forEach((i) => { if (!(isPlainObject(i) || isString(i))) { throw new Error('Invalid param: array "request.files" must contain objects or strings.'); } }); } if (isString(files)) { this.request.hasSingleFileResponse = true; const file = this.context.metafile.data.files.find( (i) => i.id === files ); if (!(file instanceof MetafileFile)) { throw new Error(`File not found: "${files}".`); } this.request.files = [file]; } else if (isUndefined(files)) { this.request.files = [...this.context.metafile.data.files]; } else if (isArray(files)) { this.request.files = files.map((file) => { let metafileFile; if (isString(file)) { const foundFile = this.context.metafile.data.files.find( (i) => i.id === file ); if (isUndefined(foundFile)) { throw new Error(`File not found: "${file}".`); } metafileFile = foundFile; } else { const foundFile = this.context.metafile.data.files.find( (i) => i.id === file.id ); if (isUndefined(foundFile)) { throw new Error(`File not found: "${file.id}".`); } metafileFile = foundFile; } return metafileFile; }); } else if (isPlainObject(files)) { this.request.hasSingleFileResponse = true; const foundFile = this.context.metafile.data.files.find( (i) => i.id === files.id ); if (isUndefined(foundFile)) { throw new Error(`File not found: "${files.id}".`); } this.request.files = [foundFile]; } return this; } addLocales(locales, excludeBaseLocale) { if (!(isString(locales) || isUndefined(locales) || isArray(locales))) { throw new Error('Invalid param: "request.locales" must be array, string or undefined.'); } if (isArray(locales)) { locales.forEach((i) => { if (!isString(i)) { throw new Error('Invalid param: array "request.locales" must contain strings.'); } }); } if (isString(locales)) { this.request.hasSingleLocaleResponse = true; this.request.files.reduce((acc, cur) => { acc.data[cur.id] = cur.locales.filter( (metafileLocale) => metafileLocale.locale === locales ); return acc; }, this.request.localesMap); } else if (isUndefined(locales)) { this.request.files.reduce((acc, cur) => { acc.data[cur.id] = excludeBaseLocale ? cur.locales.filter( (metafileLocale) => !metafileLocale.isBaseLocale ) : cur.locales; return acc; }, this.request.localesMap); } else if (isArray(locales)) { this.request.files.reduce((acc, cur) => { acc.data[cur.id] = cur.locales.filter( (metafileLocale) => locales.includes(metafileLocale.locale) ); return acc; }, this.request.localesMap); } return this; } getCdnRequest() { const result = this.request; this.request = new Request(this.context); return result; } } class CdnClient { metafile; cache; context; static version = "1.5.13"; constructor(options) { const metafileContext = new MetafileContext(options); const client = new FetchHttpAdapter(metafileContext.params.baseUrl); this.context = new Context({ metafileContext, cdn: this, client }); this.metafile = new CdnMetafile(this.context); this.cache = new CdnCache(this.context); } fetch = async (options) => { const { files, locales, excludeBaseLocale } = options || {}; const requestBuilder = new RequestBuilder(this.context).addFiles(files).addLocales(locales, excludeBaseLocale); return requestBuilder.getCdnRequest().execute(); }; static async create(options) { if (!options) { throw new Error('Invalid param: missing required "options" parameter.'); } if (!isString(options.metafile)) { throw new Error('Invalid param: "options.metafile" must be string.'); } const cdn = new CdnClient(options); await cdn.metafile.refresh(); return cdn; } } export { Api, CdnBase, CdnCache, CdnClient, CdnMetafile, Context, FetchHttpAdapter, LocalesCache, LocalesMap, MemoryCacheAdapter, MetafileContext, MetafileData, MetafileFile, MetafileLocale, MetafileParams, Request, RequestBuilder, ResponseFactory, isArray, isPlainObject, isString, isUndefined, uniq, uniqBy }; //# sourceMappingURL=localazy-cdn-client.js.map