"use strict"; /** * General notes: * - Raw/`raw_` data *should* be left as-is from `DataUtil`, such that we match anything returned by a prop-specific * `.loadRawJSON` in `DataUtil`. Note that this is generally *not* the same as the result of `DataUtil.loadRawJSON`, * which is instead JSON prior to the application of `_copy`/etc.! * - Other cached data (without `raw_`) should be ready for use, with all references resolved to the best of our * capabilities. */ // region Utilities class _DataLoaderConst { static SOURCE_SITE_ALL = Symbol("SOURCE_SITE_ALL"); static SOURCE_PRERELEASE_ALL_CURRENT = Symbol("SOURCE_PRERELEASE_ALL_CURRENT"); static SOURCE_BREW_ALL_CURRENT = Symbol("SOURCE_BREW_ALL_CURRENT"); static ENTITY_NULL = Symbol("ENTITY_NULL"); } class _DataLoaderInternalUtil { static getCleanPageSourceHash ({page, source, hash}) { return { page: this.getCleanPage({page}), source: this.getCleanSource({source}), hash: this.getCleanHash({hash}), }; } static getCleanPage ({page}) { return page.toLowerCase(); } static getCleanSource ({source}) { return source.toLowerCase(); } static getCleanHash ({hash}) { return hash.toLowerCase(); } /* -------------------------------------------- */ static getCleanPageFluff ({page}) { return `${this.getCleanPage({page})}fluff`; } /* -------------------------------------------- */ static _NOTIFIED_FAILED_DEREFERENCES = new Set(); static doNotifyFailedDereferences ({missingRefSets, diagnostics}) { // region Avoid repeatedly throwing errors for the same missing references const missingRefSetsUnseen = Object.entries(missingRefSets) .mergeMap(([prop, set]) => ({ [prop]: new Set( [...set] .filter(ref => { const refLower = ref.toLowerCase(); const out = !this._NOTIFIED_FAILED_DEREFERENCES.has(refLower); this._NOTIFIED_FAILED_DEREFERENCES.add(refLower); return out; }), ), })); // endregion const cntMissingRefs = Object.values(missingRefSetsUnseen).map(({size}) => size).sum(); if (!cntMissingRefs) return; const notificationRefs = Object.entries(missingRefSetsUnseen) .map(([k, v]) => `${k}: ${[...v].sort(SortUtil.ascSortLower).join(", ")}`) .join("; "); const ptDiagnostics = DataLoader.getDiagnosticsSummary(diagnostics); const msgStart = `Failed to load references for ${cntMissingRefs} entr${cntMissingRefs === 1 ? "y" : "ies"}!`; JqueryUtil.doToast({ type: "danger", content: `${msgStart} Reference types and values were: ${[notificationRefs, ptDiagnostics].join(" ")}`, isAutoHide: false, }); const cnslRefs = [ ...Object.entries(missingRefSetsUnseen) .map(([k, v]) => `${k}:\n\t${[...v].sort(SortUtil.ascSortLower).join("\n\t")}`), ptDiagnostics, ] .filter(Boolean) .join("\n"); setTimeout(() => { throw new Error(`${msgStart}\nReference types and values were:\n${cnslRefs}`); }); } } // endregion /* -------------------------------------------- */ // region Dereferencer class _DataLoaderDereferencerBase { static _DereferenceMeta = class { constructor ({cntReplaces = 0, offsetIx = 0}) { this.cntReplaces = cntReplaces; this.offsetIx = offsetIx; } }; static _WALKER_MOD = MiscUtil.getWalker({ keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, }); /* -------------------------------------------- */ _pPreloadingRefContentSite = null; _pPreloadingRefContentPrerelease = null; _pPreloadingRefContentBrew = null; _preloadingPrereleaseLastIdent = null; _preloadingBrewLastIdent = null; async pPreloadRefContent () { await (this._pPreloadingRefContentSite = this._pPreloadingRefContentSite || this._pPreloadRefContentSite()); if (typeof PrereleaseUtil !== "undefined") { const identPrerelease = PrereleaseUtil.getCacheIteration(); if (identPrerelease !== this._preloadingPrereleaseLastIdent) this._pPreloadingRefContentPrerelease = null; this._preloadingPrereleaseLastIdent = identPrerelease; await (this._pPreloadingRefContentPrerelease = this._pPreloadingRefContentPrerelease || this._pPreloadRefContentPrerelease()); } if (typeof BrewUtil2 !== "undefined") { const identBrew = BrewUtil2.getCacheIteration(); if (identBrew !== this._preloadingBrewLastIdent) this._pPreloadingRefContentBrew = null; this._preloadingBrewLastIdent = identBrew; await (this._pPreloadingRefContentBrew = this._pPreloadingRefContentBrew || this._pPreloadRefContentBrew()); } } async _pPreloadRefContentSite () { /* Implement as required */ } async _pPreloadRefContentPrerelease () { /* Implement as required */ } async _pPreloadRefContentBrew () { /* Implement as required */ } /* -------------------------------------------- */ dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { throw new Error("Unimplemented!"); } _getCopyFromCache ({page, entriesWithoutRefs, refUnpacked, refHash}) { if (page.toLowerCase().endsWith(".html")) throw new Error(`Could not dereference "${page}" content. Dereferencing is only supported for props!`); // Prefer content from our active load, where available return entriesWithoutRefs[page]?.[refHash] ? MiscUtil.copyFast(entriesWithoutRefs[page]?.[refHash]) : DataLoader.getFromCache(page, refUnpacked.source, refHash, {isCopy: true}); } } class _DataLoaderDereferencerClassSubclassFeatures extends _DataLoaderDereferencerBase { dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { const prop = toReplaceMeta.type === "refClassFeature" ? "classFeature" : "subclassFeature"; const refUnpacked = toReplaceMeta.type === "refClassFeature" ? DataUtil.class.unpackUidClassFeature(toReplaceMeta.classFeature) : DataUtil.class.unpackUidSubclassFeature(toReplaceMeta.subclassFeature); const refHash = UrlUtil.URL_TO_HASH_BUILDER[prop](refUnpacked); // Skip blocklisted if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(refHash, prop, refUnpacked.source, {isNoCount: true})) { toReplaceMeta.array[toReplaceMeta.ix] = {}; return new this.constructor._DereferenceMeta({cntReplaces: 1}); } const cpy = this._getCopyFromCache({page: prop, entriesWithoutRefs, refUnpacked, refHash}); if (!cpy) return new this.constructor._DereferenceMeta({cntReplaces: 0}); delete cpy.header; if (toReplaceMeta.name) cpy.name = toReplaceMeta.name; toReplaceMeta.array[toReplaceMeta.ix] = cpy; return new this.constructor._DereferenceMeta({cntReplaces: 1}); } } class _DataLoaderDereferencerOptionalfeatures extends _DataLoaderDereferencerBase { async _pPreloadRefContentSite () { await DataLoader.pCacheAndGetAllSite(UrlUtil.PG_OPT_FEATURES); } async _pPreloadRefContentPrerelease () { await DataLoader.pCacheAndGetAllPrerelease(UrlUtil.PG_OPT_FEATURES); } async _pPreloadRefContentBrew () { await DataLoader.pCacheAndGetAllBrew(UrlUtil.PG_OPT_FEATURES); } dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { const refUnpacked = DataUtil.generic.unpackUid(toReplaceMeta.optionalfeature, "optfeature"); const refHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES](refUnpacked); // Skip blocklisted if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(refHash, "optionalfeature", refUnpacked.source, {isNoCount: true})) { toReplaceMeta.array[toReplaceMeta.ix] = {}; return new this.constructor._DereferenceMeta({cntReplaces: 1}); } const cpy = this._getCopyFromCache({page: "optionalfeature", entriesWithoutRefs, refUnpacked, refHash}); if (!cpy) return new this.constructor._DereferenceMeta({cntReplaces: 0}); delete cpy.featureType; delete cpy.prerequisite; if (toReplaceMeta.name) cpy.name = toReplaceMeta.name; toReplaceMeta.array[toReplaceMeta.ix] = cpy; return new this.constructor._DereferenceMeta({cntReplaces: 1}); } } class _DataLoaderDereferencerItemEntries extends _DataLoaderDereferencerBase { async _pPreloadRefContentSite () { await DataLoader.pCacheAndGetAllSite(UrlUtil.PG_ITEMS); } async _pPreloadRefContentPrerelease () { await DataLoader.pCacheAndGetAllPrerelease(UrlUtil.PG_ITEMS); } async _pPreloadRefContentBrew () { await DataLoader.pCacheAndGetAllBrew(UrlUtil.PG_ITEMS); } dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { const refUnpacked = DataUtil.generic.unpackUid(toReplaceMeta.itemEntry, "itemEntry"); const refHash = UrlUtil.URL_TO_HASH_BUILDER["itemEntry"](refUnpacked); const cpy = this._getCopyFromCache({page: "itemEntry", entriesWithoutRefs, refUnpacked, refHash}); if (!cpy) return new this.constructor._DereferenceMeta({cntReplaces: 0}); cpy.entriesTemplate = this.constructor._WALKER_MOD.walk( cpy.entriesTemplate, { string: (str) => { return Renderer.utils.applyTemplate( ent, str, ); }, }, ); toReplaceMeta.array.splice(toReplaceMeta.ix, 1, ...cpy.entriesTemplate); return new this.constructor._DereferenceMeta({ cntReplaces: 1, // Offset by the length of the array we just merged in (minus one, since we replaced an // element) offsetIx: cpy.entriesTemplate.length - 1, }); } } class _DataLoaderDereferencer { static _REF_TYPE_TO_DEREFERENCER = {}; static _init () { this._REF_TYPE_TO_DEREFERENCER["refClassFeature"] = this._REF_TYPE_TO_DEREFERENCER["refSubclassFeature"] = new _DataLoaderDereferencerClassSubclassFeatures(); this._REF_TYPE_TO_DEREFERENCER["refOptionalfeature"] = new _DataLoaderDereferencerOptionalfeatures(); this._REF_TYPE_TO_DEREFERENCER["refItemEntry"] = new _DataLoaderDereferencerItemEntries(); return null; } static _ = this._init(); static _WALKER_READ = MiscUtil.getWalker({ keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, isNoModification: true, isBreakOnReturn: true, }); /** * Build an object of the form `{page: [...entities...]}` and return it. * @param entities * @param {string} page * @param {string} propEntries * @param {string} propIsRef */ static async pGetDereferenced ( entities, page, { propEntries = "entries", propIsRef = null, } = {}, ) { if (page.toLowerCase().endsWith(".html")) throw new Error(`Could not dereference "${page}" content. Dereferencing is only supported for props!`); if (!entities || !entities.length) return {}; const out = {}; const entriesWithRefs = {}; const entriesWithoutRefs = {}; this._pGetDereferenced_doSegregateWithWithoutRefs({ entities, page, propEntries, propIsRef, entriesWithRefs, entriesWithoutRefs, }); await this._pGetDereferenced_pDoDereference({propEntries, entriesWithRefs, entriesWithoutRefs}); this._pGetDereferenced_doNotifyFailed({entriesWithRefs, entities}); this._pGetDereferenced_doPopulateOutput({page, out, entriesWithoutRefs, entriesWithRefs}); return out; } /* -------------------------------------------- */ static _pGetDereferenced_doSegregateWithWithoutRefs ({entities, page, propEntries, propIsRef, entriesWithRefs, entriesWithoutRefs}) { const hashBuilder = UrlUtil.URL_TO_HASH_BUILDER[page]; entities .forEach(ent => { const hash = hashBuilder(ent); const hasRefs = this._pGetDereferenced_hasRefs({ent, propEntries, propIsRef}); ( (hasRefs ? entriesWithRefs : entriesWithoutRefs)[page] = (hasRefs ? entriesWithRefs : entriesWithoutRefs)[page] || {} )[hash] = hasRefs ? MiscUtil.copyFast(ent) : ent; }); } static _pGetDereferenced_hasRefs ({ent, propEntries, propIsRef}) { if (propIsRef != null) return !!ent[propIsRef]; const ptrHasRef = {_: false}; this._WALKER_READ.walk(ent[propEntries], this._pGetDereferenced_doPopulateRaw_getHandlers({ptrHasRef})); return ptrHasRef._; } static _pGetDereferenced_doPopulateRaw_getHandlers ({ptrHasRef}) { return { object: (obj) => { if (this._REF_TYPE_TO_DEREFERENCER[obj.type]) return ptrHasRef._ = true; }, string: (str) => { if (str.startsWith("{#") && str.endsWith("}")) return ptrHasRef._ = true; }, }; } /* -------------------------------------------- */ static _MAX_DEREFERENCE_LOOPS = 25; // conservatively avoid infinite looping static async _pGetDereferenced_pDoDereference ({propEntries, entriesWithRefs, entriesWithoutRefs}) { for (let i = 0; i < this._MAX_DEREFERENCE_LOOPS; ++i) { if (!Object.keys(entriesWithRefs).length) break; for (const [page, pageEntries] of Object.entries(entriesWithRefs)) { for (const [hash, ent] of Object.entries(pageEntries)) { const toReplaceMetas = []; this._WALKER_READ.walk( ent[propEntries], this._pGetDereferenced_doDereference_getHandlers({toReplaceMetas}), ); for (const {type} of toReplaceMetas) { if (!this._REF_TYPE_TO_DEREFERENCER[type]) continue; await this._REF_TYPE_TO_DEREFERENCER[type].pPreloadRefContent(); } let cntReplaces = 0; for (let ixReplace = 0; ixReplace < toReplaceMetas.length; ++ixReplace) { const toReplaceMeta = this._pGetDereferenced_doDereference_getToReplaceMeta(toReplaceMetas[ixReplace]); const derefMeta = this._REF_TYPE_TO_DEREFERENCER[toReplaceMeta.type].dereference({ ent, entriesWithoutRefs, toReplaceMeta, ixReplace, }); cntReplaces += derefMeta.cntReplaces; if (!derefMeta.offsetIx) continue; toReplaceMetas.slice(ixReplace + 1).forEach(it => it.ix += derefMeta.offsetIx); } if (cntReplaces === toReplaceMetas.length) { delete pageEntries[hash]; (entriesWithoutRefs[page] = entriesWithoutRefs[page] || {})[hash] = ent; } } if (!Object.keys(pageEntries).length) delete entriesWithRefs[page]; } } } static _pGetDereferenced_doDereference_getHandlers ({toReplaceMetas}) { return { array: (arr) => { arr.forEach((it, i) => { if (this._REF_TYPE_TO_DEREFERENCER[it.type]) { toReplaceMetas.push({ ...it, array: arr, ix: i, }); return; } if (typeof it === "string" && it.startsWith("{#") && it.endsWith("}")) { toReplaceMetas.push({ string: it, array: arr, ix: i, }); } }); }, }; } static _pGetDereferenced_doDereference_getToReplaceMeta (toReplaceMetaRaw) { if (toReplaceMetaRaw.string == null) return toReplaceMetaRaw; const str = toReplaceMetaRaw.string; delete toReplaceMetaRaw.string; return {...toReplaceMetaRaw, ...Renderer.hover.getRefMetaFromTag(str)}; } /* -------------------------------------------- */ static _pGetDereferenced_doNotifyFailed ({entriesWithRefs, entities}) { const entriesWithRefsVals = Object.values(entriesWithRefs) .map(hashToEntry => Object.values(hashToEntry)) .flat(); if (!entriesWithRefsVals.length) return; const missingRefSets = {}; this._WALKER_READ.walk( entriesWithRefsVals, { object: (obj) => { switch (obj.type) { case "refClassFeature": (missingRefSets["classFeature"] = missingRefSets["classFeature"] || new Set()).add(obj.classFeature); break; case "refSubclassFeature": (missingRefSets["subclassFeature"] = missingRefSets["subclassFeature"] || new Set()).add(obj.subclassFeature); break; case "refOptionalfeature": (missingRefSets["optionalfeature"] = missingRefSets["optionalfeature"] || new Set()).add(obj.optionalfeature); break; case "refItemEntry": (missingRefSets["itemEntry"] = missingRefSets["itemEntry"] || new Set()).add(obj.itemEntry); break; } }, }, ); _DataLoaderInternalUtil.doNotifyFailedDereferences({ missingRefSets, diagnostics: entities .map(ent => ent.__diagnostic) .filter(Boolean), }); } /* -------------------------------------------- */ static _pGetDereferenced_doPopulateOutput ({isOverwrite, out, entriesWithoutRefs, entriesWithRefs}) { [ ...Object.entries(entriesWithoutRefs), // Add the failed-to-resolve entities to the cache; the missing refs will simply not be rendered ...Object.entries(entriesWithRefs), ] .forEach(([page, hashToEnt]) => { Object.entries(hashToEnt) .forEach(([hash, ent]) => { if (!isOverwrite && DataLoader.getFromCache(page, ent.source, hash)) return; (out[page] = out[page] || []).push(ent); }); }); } } // endregion /* -------------------------------------------- */ // region Cache class _DataLoaderCache { static _PARTITION_UNKNOWN = 0; static _PARTITION_SITE = 1; static _PARTITION_PRERELEASE = 2; static _PARTITION_BREW = 3; _cache = {}; _cacheSiteLists = {}; _cachePrereleaseLists = {}; _cacheBrewLists = {}; get (pageClean, sourceClean, hashClean) { return this._cache[pageClean]?.[sourceClean]?.[hashClean]; } getAllSite (pageClean) { return Object.values(this._cacheSiteLists[pageClean] || {}); } getAllPrerelease (pageClean) { return Object.values(this._cachePrereleaseLists[pageClean] || {}); } getAllBrew (pageClean) { return Object.values(this._cacheBrewLists[pageClean] || {}); } set (pageClean, sourceClean, hashClean, ent) { // region Set primary cache let pageCache = this._cache[pageClean]; if (!pageCache) { pageCache = {}; this._cache[pageClean] = pageCache; } let sourceCache = pageCache[sourceClean]; if (!sourceCache) { sourceCache = {}; pageCache[sourceClean] = sourceCache; } sourceCache[hashClean] = ent; // endregion if (ent === _DataLoaderConst.ENTITY_NULL) return; // region Set site/prerelease/brew list cache switch (this._set_getPartition(ent)) { case this.constructor._PARTITION_SITE: { return this._set_addToPartition({ cache: this._cacheSiteLists, pageClean, hashClean, ent, }); } case this.constructor._PARTITION_PRERELEASE: { return this._set_addToPartition({ cache: this._cachePrereleaseLists, pageClean, hashClean, ent, }); } case this.constructor._PARTITION_BREW: { return this._set_addToPartition({ cache: this._cacheBrewLists, pageClean, hashClean, ent, }); } // Skip by default } // endregion } _set_getPartition (ent) { if (ent.adventure) return this._set_getPartition_fromSource(SourceUtil.getEntitySource(ent.adventure)); if (ent.book) return this._set_getPartition_fromSource(SourceUtil.getEntitySource(ent.book)); if (ent.__prop !== "item" || ent._category !== "Specific Variant") return this._set_getPartition_fromSource(SourceUtil.getEntitySource(ent)); // "Specific Variant" items have a dual source. For the purposes of partitioning: // - only items with both `baseitem` source and `magicvariant` source both "site" sources // - items which include any brew are treated as brew // - items which include any prerelease (and no brew) are treated as prerelease const entitySource = SourceUtil.getEntitySource(ent); const partitionBaseitem = this._set_getPartition_fromSource(entitySource); const partitionMagicvariant = this._set_getPartition_fromSource(ent._baseSource ?? entitySource); if (partitionBaseitem === partitionMagicvariant && partitionBaseitem === this.constructor._PARTITION_SITE) return this.constructor._PARTITION_SITE; if (partitionBaseitem === this.constructor._PARTITION_BREW || partitionMagicvariant === this.constructor._PARTITION_BREW) return this.constructor._PARTITION_BREW; return this.constructor._PARTITION_PRERELEASE; } _set_getPartition_fromSource (partitionSource) { if (SourceUtil.isSiteSource(partitionSource)) return this.constructor._PARTITION_SITE; if (PrereleaseUtil.hasSourceJson(partitionSource)) return this.constructor._PARTITION_PRERELEASE; if (BrewUtil2.hasSourceJson(partitionSource)) return this.constructor._PARTITION_BREW; return this.constructor._PARTITION_UNKNOWN; } _set_addToPartition ({cache, pageClean, hashClean, ent}) { let siteListCache = cache[pageClean]; if (!siteListCache) { siteListCache = {}; cache[pageClean] = siteListCache; } siteListCache[hashClean] = ent; } } // endregion /* -------------------------------------------- */ // region Data type loading class _DataTypeLoader { static PROPS = []; static PAGE = null; static IS_FLUFF = false; static register ({fnRegister}) { fnRegister({ loader: new this(), props: this.PROPS, page: this.PAGE, isFluff: this.IS_FLUFF, }); } static _getAsRawPrefixed (json, {propsRaw}) { return { ...propsRaw.mergeMap(prop => ({[`raw_${prop}`]: json[prop]})), }; } /* -------------------------------------------- */ /** Used to reduce phase 1 caching for a loader where phase 2 is the primary caching step. */ phase1CachePropAllowlist; /** (Unused) */ phase2CachePropAllowlist; hasPhase2Cache = false; _cache_pSiteData = {}; _cache_pPostCaches = {}; /** * @param pageClean * @param sourceClean * @return {string} */ _getSiteIdent ({pageClean, sourceClean}) { throw new Error("Unimplemented!"); } _isPrereleaseAvailable () { return typeof PrereleaseUtil !== "undefined"; } _isBrewAvailable () { return typeof BrewUtil2 !== "undefined"; } async _pPrePopulate ({data, isPrerelease, isBrew}) { /* Implement as required */ } async pGetSiteData ({pageClean, sourceClean}) { const propCache = this._getSiteIdent({pageClean, sourceClean}); this._cache_pSiteData[propCache] = this._cache_pSiteData[propCache] || this._pGetSiteData({pageClean, sourceClean}); return this._cache_pSiteData[propCache]; } async _pGetSiteData ({pageClean, sourceClean}) { throw new Error("Unimplemented!"); } async pGetStoredPrereleaseData () { if (!this._isPrereleaseAvailable()) return {}; return this._pGetStoredPrereleaseData(); } async pGetStoredBrewData () { if (!this._isBrewAvailable()) return {}; return this._pGetStoredBrewData(); } async _pGetStoredPrereleaseData () { return this._pGetStoredPrereleaseBrewData({brewUtil: PrereleaseUtil, isPrerelease: true}); } async _pGetStoredBrewData () { return this._pGetStoredPrereleaseBrewData({brewUtil: BrewUtil2, isBrew: true}); } async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { const prereleaseBrewData = await brewUtil.pGetBrewProcessed(); await this._pPrePopulate({data: prereleaseBrewData, isPrerelease, isBrew}); return prereleaseBrewData; } async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { /* Implement as required */ } async _pGetPostCacheData_obj_withCache ({obj, propCache, lockToken2}) { this._cache_pPostCaches[propCache] = this._cache_pPostCaches[propCache] || this._pGetPostCacheData_obj({obj, lockToken2}); return this._cache_pPostCaches[propCache]; } async _pGetPostCacheData_obj ({obj, lockToken2}) { throw new Error("Unimplemented!"); } hasCustomCacheStrategy ({obj}) { return false; } addToCacheCustom ({cache, obj}) { /* Implement as required */ } } class _DataTypeLoaderSingleSource extends _DataTypeLoader { _filename; _getSiteIdent ({pageClean, sourceClean}) { return this._filename; } async _pGetSiteData ({pageClean, sourceClean}) { return DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${this._filename}`); } } class _DataTypeLoaderBackground extends _DataTypeLoaderSingleSource { static PROPS = ["background"]; static PAGE = UrlUtil.PG_BACKGROUNDS; _filename = "backgrounds.json"; } class _DataTypeLoaderPsionic extends _DataTypeLoaderSingleSource { static PROPS = ["psionic"]; static PAGE = UrlUtil.PG_PSIONICS; _filename = "psionics.json"; } class _DataTypeLoaderObject extends _DataTypeLoaderSingleSource { static PROPS = ["object"]; static PAGE = UrlUtil.PG_OBJECTS; _filename = "objects.json"; } class _DataTypeLoaderAction extends _DataTypeLoaderSingleSource { static PROPS = ["action"]; static PAGE = UrlUtil.PG_ACTIONS; _filename = "actions.json"; } class _DataTypeLoaderFeat extends _DataTypeLoaderSingleSource { static PROPS = ["feat"]; static PAGE = UrlUtil.PG_FEATS; _filename = "feats.json"; } class _DataTypeLoaderOptionalfeature extends _DataTypeLoaderSingleSource { static PROPS = ["optionalfeature"]; static PAGE = UrlUtil.PG_OPT_FEATURES; _filename = "optionalfeatures.json"; } class _DataTypeLoaderReward extends _DataTypeLoaderSingleSource { static PROPS = ["reward"]; static PAGE = UrlUtil.PG_REWARDS; _filename = "rewards.json"; } class _DataTypeLoaderCharoption extends _DataTypeLoaderSingleSource { static PROPS = ["charoption"]; static PAGE = UrlUtil.PG_CHAR_CREATION_OPTIONS; _filename = "charcreationoptions.json"; } class _DataTypeLoaderTrapHazard extends _DataTypeLoaderSingleSource { static PROPS = ["trap", "hazard"]; static PAGE = UrlUtil.PG_TRAPS_HAZARDS; _filename = "trapshazards.json"; } class _DataTypeLoaderCultBoon extends _DataTypeLoaderSingleSource { static PROPS = ["cult", "boon"]; static PAGE = UrlUtil.PG_CULTS_BOONS; _filename = "cultsboons.json"; } class _DataTypeLoaderVehicle extends _DataTypeLoaderSingleSource { static PROPS = ["vehicle", "vehicleUpgrade"]; static PAGE = UrlUtil.PG_VEHICLES; _filename = "vehicles.json"; } class _DataTypeLoaderConditionDisease extends _DataTypeLoaderSingleSource { static PROPS = ["condition", "disease", "status"]; static PAGE = UrlUtil.PG_CONDITIONS_DISEASES; _filename = "conditionsdiseases.json"; } class _DataTypeLoaderSkill extends _DataTypeLoaderSingleSource { static PROPS = ["skill"]; _filename = "skills.json"; } class _DataTypeLoaderSense extends _DataTypeLoaderSingleSource { static PROPS = ["sense"]; _filename = "senses.json"; } class _DataTypeLoaderLegendaryGroup extends _DataTypeLoaderSingleSource { static PROPS = ["legendaryGroup"]; _filename = "bestiary/legendarygroups.json"; } class _DataTypeLoaderItemEntry extends _DataTypeLoaderSingleSource { static PROPS = ["itemEntry"]; _filename = "items-base.json"; } class _DataTypeLoaderItemMastery extends _DataTypeLoaderSingleSource { static PROPS = ["itemMastery"]; _filename = "items-base.json"; async _pPrePopulate ({data, isPrerelease, isBrew}) { // Ensure properties are loaded await Renderer.item.pGetSiteUnresolvedRefItems(); Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({data}); } } class _DataTypeLoaderBackgroundFluff extends _DataTypeLoaderSingleSource { static PROPS = ["backgroundFluff"]; static PAGE = UrlUtil.PG_BACKGROUNDS; static IS_FLUFF = true; _filename = "fluff-backgrounds.json"; } class _DataTypeLoaderFeatFluff extends _DataTypeLoaderSingleSource { static PROPS = ["featFluff"]; static PAGE = UrlUtil.PG_FEATS; static IS_FLUFF = true; _filename = "fluff-feats.json"; } class _DataTypeLoaderItemFluff extends _DataTypeLoaderSingleSource { static PROPS = ["itemFluff"]; static PAGE = UrlUtil.PG_ITEMS; static IS_FLUFF = true; _filename = "fluff-items.json"; } class _DataTypeLoaderRaceFluff extends _DataTypeLoaderSingleSource { static PROPS = ["raceFluff"]; static PAGE = UrlUtil.PG_RACES; static IS_FLUFF = true; _filename = "fluff-races.json"; } class _DataTypeLoaderLanguageFluff extends _DataTypeLoaderSingleSource { static PROPS = ["languageFluff"]; static PAGE = UrlUtil.PG_LANGUAGES; static IS_FLUFF = true; _filename = "fluff-languages.json"; } class _DataTypeLoaderVehicleFluff extends _DataTypeLoaderSingleSource { static PROPS = ["vehicleFluff"]; static PAGE = UrlUtil.PG_VEHICLES; static IS_FLUFF = true; _filename = "fluff-vehicles.json"; } class _DataTypeLoaderObjectFluff extends _DataTypeLoaderSingleSource { static PROPS = ["objectFluff"]; static PAGE = UrlUtil.PG_OBJECTS; static IS_FLUFF = true; _filename = "fluff-objects.json"; } class _DataTypeLoaderCharoptionFluff extends _DataTypeLoaderSingleSource { static PROPS = ["charoptionFluff"]; static PAGE = UrlUtil.PG_CHAR_CREATION_OPTIONS; static IS_FLUFF = true; _filename = "fluff-charcreationoptions.json"; } class _DataTypeLoaderRecipeFluff extends _DataTypeLoaderSingleSource { static PROPS = ["recipeFluff"]; static PAGE = UrlUtil.PG_RECIPES; static IS_FLUFF = true; _filename = "fluff-recipes.json"; } class _DataTypeLoaderConditionDiseaseFluff extends _DataTypeLoaderSingleSource { static PROPS = ["conditionFluff", "diseaseFluff", "statusFluff"]; static PAGE = UrlUtil.PG_CONDITIONS_DISEASES; static IS_FLUFF = true; _filename = "fluff-conditionsdiseases.json"; } class _DataTypeLoaderTrapHazardFluff extends _DataTypeLoaderSingleSource { static PROPS = ["trapFluff", "hazardFluff"]; static PAGE = UrlUtil.PG_TRAPS_HAZARDS; static IS_FLUFF = true; _filename = "fluff-trapshazards.json"; } class _DataTypeLoaderPredefined extends _DataTypeLoader { _loader; _loadJsonArgs = null; _loadPrereleaseArgs = null; _loadBrewArgs = null; _getSiteIdent ({pageClean, sourceClean}) { return this._loader; } async _pGetSiteData ({pageClean, sourceClean}) { return DataUtil[this._loader].loadJSON(this._loadJsonArgs); } async _pGetStoredPrereleaseData () { if (!DataUtil[this._loader].loadPrerelease) return super._pGetStoredPrereleaseData(); return DataUtil[this._loader].loadPrerelease(this._loadPrereleaseArgs); } async _pGetStoredBrewData () { if (!DataUtil[this._loader].loadBrew) return super._pGetStoredBrewData(); return DataUtil[this._loader].loadBrew(this._loadBrewArgs); } } class _DataTypeLoaderRace extends _DataTypeLoaderPredefined { static PROPS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_RACES]]; static PAGE = UrlUtil.PG_RACES; _loader = "race"; _loadJsonArgs = {isAddBaseRaces: true}; _loadPrereleaseArgs = {isAddBaseRaces: true}; _loadBrewArgs = {isAddBaseRaces: true}; } class _DataTypeLoaderDeity extends _DataTypeLoaderPredefined { static PROPS = ["deity"]; static PAGE = UrlUtil.PG_DEITIES; _loader = "deity"; } class _DataTypeLoaderVariantrule extends _DataTypeLoaderPredefined { static PROPS = ["variantrule"]; static PAGE = UrlUtil.PG_VARIANTRULES; _loader = "variantrule"; } class _DataTypeLoaderTable extends _DataTypeLoaderPredefined { static PROPS = ["table", "tableGroup"]; static PAGE = UrlUtil.PG_TABLES; _loader = "table"; } class _DataTypeLoaderLanguage extends _DataTypeLoaderPredefined { static PROPS = ["language"]; static PAGE = UrlUtil.PG_LANGUAGES; _loader = "language"; } class _DataTypeLoaderRecipe extends _DataTypeLoaderPredefined { static PROPS = ["recipe"]; static PAGE = UrlUtil.PG_RECIPES; _loader = "recipe"; } class _DataTypeLoaderMultiSource extends _DataTypeLoader { _prop; _getSiteIdent ({pageClean, sourceClean}) { // use `.toString()` in case `sourceClean` is a `Symbol` return `${this._prop}__${sourceClean.toString()}`; } async _pGetSiteData ({pageClean, sourceClean}) { const data = await this._pGetSiteData_data({sourceClean}); if (data == null) return {}; await this._pPrePopulate({data}); return data; } async _pGetSiteData_data ({sourceClean}) { if (sourceClean === _DataLoaderConst.SOURCE_SITE_ALL) return this._pGetSiteDataAll(); const source = Parser.sourceJsonToJson(sourceClean); return DataUtil[this._prop].pLoadSingleSource(source); } async _pGetSiteDataAll () { return DataUtil[this._prop].loadJSON(); } } class _DataTypeLoaderCustomMonster extends _DataTypeLoaderMultiSource { static PROPS = ["monster"]; static PAGE = UrlUtil.PG_BESTIARY; _prop = "monster"; async _pGetSiteData ({pageClean, sourceClean}) { await DataUtil.monster.pPreloadMeta(); return super._pGetSiteData({pageClean, sourceClean}); } async _pPrePopulate ({data, isPrerelease, isBrew}) { DataUtil.monster.populateMetaReference(data); } } class _DataTypeLoaderCustomMonsterFluff extends _DataTypeLoaderMultiSource { static PROPS = ["monsterFluff"]; static PAGE = UrlUtil.PG_BESTIARY; static IS_FLUFF = true; _prop = "monsterFluff"; } class _DataTypeLoaderCustomSpell extends _DataTypeLoaderMultiSource { static PROPS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_SPELLS]]; static PAGE = UrlUtil.PG_SPELLS; _prop = "spell"; async _pPrePopulate ({data, isPrerelease, isBrew}) { Renderer.spell.prePopulateHover(data); if (isPrerelease) Renderer.spell.prePopulateHoverPrerelease(data); if (isBrew) Renderer.spell.prePopulateHoverBrew(data); } } class _DataTypeLoaderCustomSpellFluff extends _DataTypeLoaderMultiSource { static PROPS = ["spellFluff"]; static PAGE = UrlUtil.PG_SPELLS; static IS_FLUFF = true; _prop = "spellFluff"; } /** @abstract */ class _DataTypeLoaderCustomRawable extends _DataTypeLoader { static _PROPS_RAWABLE; hasPhase2Cache = true; _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${this.constructor.name}`; } async _pGetSiteData ({pageClean, sourceClean}) { const json = await this._pGetRawSiteData(); return this.constructor._getAsRawPrefixed(json, {propsRaw: this.constructor._PROPS_RAWABLE}); } /** @abstract */ async _pGetRawSiteData () { throw new Error("Unimplemented!"); } async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { const prereleaseBrew = await brewUtil.pGetBrewProcessed(); return this.constructor._getAsRawPrefixed(prereleaseBrew, {propsRaw: this.constructor._PROPS_RAWABLE}); } static _pGetDereferencedData_doNotifyFailed ({ent, uids, prop}) { const missingRefSets = { [prop]: new Set(uids), }; _DataLoaderInternalUtil.doNotifyFailedDereferences({ missingRefSets, diagnostics: [ent.__diagnostic].filter(Boolean), }); } } class _DataTypeLoaderCustomClassesSubclass extends _DataTypeLoaderCustomRawable { static PROPS = ["raw_class", "raw_subclass", "class", "subclass"]; static PAGE = UrlUtil.PG_CLASSES; // Note that this only loads these specific props, to avoid deadlock incurred by dereferencing class/subclass features static _PROPS_RAWABLE = ["class", "subclass"]; async _pGetRawSiteData () { return DataUtil.class.loadRawJSON(); } async _pGetPostCacheData_obj ({obj, lockToken2}) { if (!obj) return null; const out = {}; if (obj.raw_class?.length) out.class = await obj.raw_class.pSerialAwaitMap(cls => this.constructor._pGetDereferencedClassData(cls, {lockToken2})); if (obj.raw_subclass?.length) out.subclass = await obj.raw_subclass.pSerialAwaitMap(sc => this.constructor._pGetDereferencedSubclassData(sc, {lockToken2})); return out; } static _mutEntryNestLevel (feature) { const depth = (feature.header == null ? 1 : feature.header) - 1; for (let i = 0; i < depth; ++i) { const nxt = MiscUtil.copyFast(feature); feature.entries = [nxt]; delete feature.name; delete feature.page; delete feature.source; } } static async _pGetDereferencedClassData (cls, {lockToken2}) { // Gracefully handle legacy class data if (cls.classFeatures && cls.classFeatures.every(it => typeof it !== "string" && !it.classFeature)) return cls; cls = MiscUtil.copyFast(cls); const byLevel = await this._pGetDereferencedClassSubclassData( cls, { lockToken2, propFeatures: "classFeatures", propFeature: "classFeature", fnUnpackUid: DataUtil.class.unpackUidClassFeature.bind(DataUtil.class), fnIsInvalidUnpackedUid: ({name, className, level}) => !name || !className || !level || isNaN(level), }, ); cls.classFeatures = [...new Array(Math.max(0, ...Object.keys(byLevel).map(Number)))] .map((_, i) => byLevel[i + 1] || []); return cls; } static async _pGetDereferencedSubclassData (sc, {lockToken2}) { // Gracefully handle legacy class data if (sc.subclassFeatures && sc.subclassFeatures.every(it => typeof it !== "string" && !it.subclassFeature)) return sc; sc = MiscUtil.copyFast(sc); const byLevel = await this._pGetDereferencedClassSubclassData( sc, { lockToken2, propFeatures: "subclassFeatures", propFeature: "subclassFeature", fnUnpackUid: DataUtil.class.unpackUidSubclassFeature.bind(DataUtil.class), fnIsInvalidUnpackedUid: ({name, className, subclassShortName, level}) => !name || !className || !subclassShortName || !level || isNaN(level), }, ); sc.subclassFeatures = Object.keys(byLevel) .map(Number) .sort(SortUtil.ascSort) .map(k => byLevel[k]); return sc; } static async _pGetDereferencedClassSubclassData ( clsOrSc, { lockToken2, propFeatures, propFeature, fnUnpackUid, fnIsInvalidUnpackedUid, }, ) { // Gracefully handle legacy data if (clsOrSc[propFeatures] && clsOrSc[propFeatures].every(it => typeof it !== "string" && !it[propFeature])) return clsOrSc; clsOrSc = MiscUtil.copyFast(clsOrSc); const byLevel = {}; // Build a map of `level: [ ...feature... ]` const notFoundUids = []; await (clsOrSc[propFeatures] || []) .pSerialAwaitMap(async featureRef => { const uid = featureRef[propFeature] ? featureRef[propFeature] : featureRef; const unpackedUid = fnUnpackUid(uid); const {source, displayText} = unpackedUid; // Skip over broken links if (fnIsInvalidUnpackedUid(unpackedUid)) return; // Skip over temp/nonexistent links if (source === Parser.SRC_5ETOOLS_TMP) return; const hash = UrlUtil.URL_TO_HASH_BUILDER[propFeature](unpackedUid); // Skip blocklisted if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(hash, propFeature, source, {isNoCount: true})) return; const feature = await DataLoader.pCacheAndGet(propFeature, source, hash, {isCopy: true, lockToken2}); // Skip over missing links if (!feature) return notFoundUids.push(uid); if (displayText) feature._displayName = displayText; if (featureRef.tableDisplayName) feature._displayNameTable = featureRef.tableDisplayName; if (featureRef.gainSubclassFeature) feature.gainSubclassFeature = true; if (featureRef.gainSubclassFeatureHasContent) feature.gainSubclassFeatureHasContent = true; if (clsOrSc.otherSources && clsOrSc.source === feature.source) feature.otherSources = MiscUtil.copyFast(clsOrSc.otherSources); this._mutEntryNestLevel(feature); (byLevel[feature.level || 1] = byLevel[feature.level || 1] || []).push(feature); }); this._pGetDereferencedData_doNotifyFailed({ent: clsOrSc, uids: notFoundUids, prop: propFeature}); return byLevel; } async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { return { siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), }; } } class _DataTypeLoaderCustomClassSubclassFeature extends _DataTypeLoader { static PROPS = ["raw_classFeature", "raw_subclassFeature", "classFeature", "subclassFeature"]; static PAGE = UrlUtil.PG_CLASS_SUBCLASS_FEATURES; static _PROPS_RAWABLE = ["classFeature", "subclassFeature"]; hasPhase2Cache = true; _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${this.constructor.name}`; } async _pGetSiteData ({pageClean, sourceClean}) { const json = await DataUtil.class.loadRawJSON(); return this.constructor._getAsRawPrefixed(json, {propsRaw: this.constructor._PROPS_RAWABLE}); } async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { const prereleaseBrew = await brewUtil.pGetBrewProcessed(); return this.constructor._getAsRawPrefixed(prereleaseBrew, {propsRaw: this.constructor._PROPS_RAWABLE}); } async _pGetPostCacheData_obj ({obj, lockToken2}) { if (!obj) return null; const out = {}; if (obj.raw_classFeature?.length) out.classFeature = (await _DataLoaderDereferencer.pGetDereferenced(obj.raw_classFeature, "classFeature"))?.classFeature || []; if (obj.raw_subclassFeature?.length) out.subclassFeature = (await _DataLoaderDereferencer.pGetDereferenced(obj.raw_subclassFeature, "subclassFeature"))?.subclassFeature || []; return out; } async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { return { siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), }; } } class _DataTypeLoaderCustomItem extends _DataTypeLoader { static PROPS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_ITEMS]]; static PAGE = UrlUtil.PG_ITEMS; /** * Avoid adding phase 1 items to the cache. Adding them as `raw_item` is inaccurate, as we have already e.g. merged * generic variants, and enhanced the items. * Adding them as `item` is also inaccurate, as we have yet to run our phase 2 post-processing to remove any * `itemEntry` references. * We could cache them under, say, `phase1_item`, but this would mean supporting `phase1_item` everywhere (has * builders, etc.), polluting other areas with our implementation details. * Therefore, cache only the essentials in phase 1. */ phase1CachePropAllowlist = new Set(["itemEntry"]); hasPhase2Cache = true; _getSiteIdent ({pageClean, sourceClean}) { return this.constructor.name; } async _pGetSiteData ({pageClean, sourceClean}) { return Renderer.item.pGetSiteUnresolvedRefItems(); } async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { const prereleaseBrewData = await brewUtil.pGetBrewProcessed(); await this._pPrePopulate({data: prereleaseBrewData, isPrerelease, isBrew}); return { item: await Renderer.item.pGetSiteUnresolvedRefItemsFromPrereleaseBrew({brewUtil, brew: prereleaseBrewData}), itemEntry: prereleaseBrewData.itemEntry || [], }; } async _pPrePopulate ({data, isPrerelease, isBrew}) { Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({data}); } async _pGetPostCacheData_obj ({siteData, obj, lockToken2}) { if (!obj) return null; const out = {}; if (obj.item?.length) { out.item = (await _DataLoaderDereferencer.pGetDereferenced(obj.item, "item", {propEntries: "entries", propIsRef: "hasRefs"}))?.item || []; out.item = (await _DataLoaderDereferencer.pGetDereferenced(out.item, "item", {propEntries: "_fullEntries", propIsRef: "hasRefs"}))?.item || []; } return out; } async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { return { siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), }; } } class _DataTypeLoaderCustomCard extends _DataTypeLoader { static PROPS = ["card"]; static PAGE = UrlUtil.PG_DECKS; _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${this.constructor.name}`; } async _pGetSiteData ({pageClean, sourceClean}) { const json = await DataUtil.deck.loadRawJSON(); return {card: json.card}; } } class _DataTypeLoaderCustomDeck extends _DataTypeLoaderCustomRawable { static PROPS = ["raw_deck", "deck"]; static PAGE = UrlUtil.PG_DECKS; static _PROPS_RAWABLE = ["deck"]; async _pGetRawSiteData () { return DataUtil.deck.loadRawJSON(); } async _pGetPostCacheData_obj ({obj, lockToken2}) { if (!obj) return null; const out = {}; if (obj.raw_deck?.length) out.deck = await obj.raw_deck.pSerialAwaitMap(ent => this.constructor._pGetDereferencedDeckData(ent, {lockToken2})); return out; } static async _pGetDereferencedDeckData (deck, {lockToken2}) { deck = MiscUtil.copyFast(deck); deck.cards = await this._pGetDereferencedCardData(deck, {lockToken2}); return deck; } static async _pGetDereferencedCardData (deck, {lockToken2}) { const notFoundUids = []; const out = (await (deck.cards || []) .pSerialAwaitMap(async cardMeta => { const uid = typeof cardMeta === "string" ? cardMeta : cardMeta.uid; const count = typeof cardMeta === "string" ? 1 : cardMeta.count ?? 1; const isReplacement = typeof cardMeta === "string" ? false : cardMeta.replacement ?? false; const unpackedUid = DataUtil.deck.unpackUidCard(uid); const {source} = unpackedUid; // Skip over broken links if (unpackedUid.name == null || unpackedUid.set == null || unpackedUid.source == null) return; const hash = UrlUtil.URL_TO_HASH_BUILDER["card"](unpackedUid); // Skip blocklisted if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(hash, "card", source, {isNoCount: true})) return; const card = await DataLoader.pCacheAndGet("card", source, hash, {isCopy: true, lockToken2}); // Skip over missing links if (!card) return notFoundUids.push(uid); if (deck.otherSources && deck.source === card.source) card.otherSources = MiscUtil.copyFast(deck.otherSources); if (isReplacement) card._isReplacement = true; return [...new Array(count)].map(() => MiscUtil.copyFast(card)); })) .flat() .filter(Boolean); this._pGetDereferencedData_doNotifyFailed({ent: deck, uids: notFoundUids, prop: "card"}); return out; } async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { return { siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), }; } } class _DataTypeLoaderCustomQuickref extends _DataTypeLoader { static PROPS = ["reference", "referenceData"]; static PAGE = UrlUtil.PG_QUICKREF; _getSiteIdent ({pageClean, sourceClean}) { return this.constructor.name; } _isPrereleaseAvailable () { return false; } _isBrewAvailable () { return false; } async _pGetSiteData ({pageClean, sourceClean}) { const json = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/bookref-quick.json`); return { reference: json.reference["bookref-quick"], referenceData: json.data["bookref-quick"], }; } hasCustomCacheStrategy ({obj}) { return this.constructor.PROPS.some(prop => obj[prop]?.length); } addToCacheCustom ({cache, obj}) { obj.referenceData.forEach((chapter, ixChapter) => this._addToCacheCustom_chapter({cache, chapter, ixChapter})); return [...this.constructor.PROPS]; } _addToCacheCustom_chapter ({cache, chapter, ixChapter}) { const metas = IndexableFileQuickReference.getChapterNameMetas(chapter, {isRequireQuickrefFlag: false}); metas.forEach(nameMeta => { const hashParts = [ "bookref-quick", ixChapter, UrlUtil.encodeForHash(nameMeta.name.toLowerCase()), ]; if (nameMeta.ixBook) hashParts.push(nameMeta.ixBook); const hash = hashParts.join(HASH_PART_SEP); const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({ page: UrlUtil.PG_QUICKREF, source: nameMeta.source, hash, }); cache.set(pageClean, sourceClean, hashClean, nameMeta.entry); if (nameMeta.ixBook) return; // region Add the hash with the redundant `0` header included hashParts.push(nameMeta.ixBook); const hashAlt = hashParts.join(HASH_PART_SEP); const hashAltClean = _DataLoaderInternalUtil.getCleanHash({hash: hashAlt}); cache.set(pageClean, sourceClean, hashAltClean, nameMeta.entry); // endregion }); } } class _DataTypeLoaderCustomAdventureBook extends _DataTypeLoader { _filename; _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${sourceClean}`; } hasCustomCacheStrategy ({obj}) { return this.constructor.PROPS.some(prop => obj[prop]?.length); } addToCacheCustom ({cache, obj}) { const [prop, propData] = this.constructor.PROPS; // Get only the ids that exist in both data + contents const dataIds = (obj[propData] || []).filter(it => it.id).map(it => it.id); const contentsIds = new Set((obj[prop] || []).filter(it => it.id).map(it => it.id)); const matchingIds = dataIds.filter(id => contentsIds.has(id)); matchingIds.forEach(id => { const data = (obj[propData] || []).find(it => it.id === id); const contents = (obj[prop] || []).find(it => it.id === id); const hash = UrlUtil.URL_TO_HASH_BUILDER[this.constructor.PAGE](contents); this._addImageBackReferences(data, this.constructor.PAGE, contents.source, hash); const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({ page: this.constructor.PAGE, source: contents.source, hash, }); const pack = { [prop]: contents, [propData]: data, }; cache.set(pageClean, sourceClean, hashClean, pack); }); return [prop, propData]; } async _pGetSiteData ({pageClean, sourceClean}) { const [prop, propData] = this.constructor.PROPS; const index = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${this._filename}`); const contents = index[prop].find(contents => _DataLoaderInternalUtil.getCleanSource({source: contents.source}) === sourceClean); if (!contents) return {}; const json = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${prop}/${prop}-${UrlUtil.encodeForHash(contents.id.toLowerCase())}.json`); return { [prop]: [contents], [propData]: [ { source: contents.source, id: contents.id, ...json, }, ], }; } _addImageBackReferences (json, page, source, hash) { if (!json) return; const walker = MiscUtil.getWalker({keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, isNoModification: true}); walker.walk( json, { object: (obj) => { if (obj.type === "image" && obj.mapRegions) { obj.page = obj.page || page; obj.source = obj.source || source; obj.hash = obj.hash || hash; } }, }, ); } } class _DataTypeLoaderCustomAdventure extends _DataTypeLoaderCustomAdventureBook { static PROPS = ["adventure", "adventureData"]; static PAGE = UrlUtil.PG_ADVENTURE; _filename = "adventures.json"; } class _DataTypeLoaderCustomBook extends _DataTypeLoaderCustomAdventureBook { static PROPS = ["book", "bookData"]; static PAGE = UrlUtil.PG_BOOK; _filename = "books.json"; } class _DataTypeLoaderCitation extends _DataTypeLoader { static PROPS = ["citation"]; _getSiteIdent ({pageClean, sourceClean}) { return this.constructor.name; } async _pGetSiteData ({pageClean, sourceClean}) { return {citation: []}; } } // endregion /* -------------------------------------------- */ // region Data loader class DataLoader { static _PROP_TO_HASH_PAGE = { "monster": UrlUtil.PG_BESTIARY, "spell": UrlUtil.PG_SPELLS, "class": UrlUtil.PG_CLASSES, "subclass": UrlUtil.PG_CLASSES, "item": UrlUtil.PG_ITEMS, "background": UrlUtil.PG_BACKGROUNDS, "psionic": UrlUtil.PG_PSIONICS, "object": UrlUtil.PG_OBJECTS, "action": UrlUtil.PG_ACTIONS, "trap": UrlUtil.PG_TRAPS_HAZARDS, "hazard": UrlUtil.PG_TRAPS_HAZARDS, "cult": UrlUtil.PG_CULTS_BOONS, "boon": UrlUtil.PG_CULTS_BOONS, "condition": UrlUtil.PG_CONDITIONS_DISEASES, "deck": UrlUtil.PG_DECKS, "disease": UrlUtil.PG_CONDITIONS_DISEASES, "status": UrlUtil.PG_CONDITIONS_DISEASES, "vehicle": UrlUtil.PG_VEHICLES, "vehicleUpgrade": UrlUtil.PG_VEHICLES, "feat": UrlUtil.PG_FEATS, "optionalfeature": UrlUtil.PG_OPT_FEATURES, "reward": UrlUtil.PG_REWARDS, "charoption": UrlUtil.PG_CHAR_CREATION_OPTIONS, "race": UrlUtil.PG_RACES, "subrace": UrlUtil.PG_RACES, "deity": UrlUtil.PG_DEITIES, "variantrule": UrlUtil.PG_VARIANTRULES, "table": UrlUtil.PG_TABLES, "tableGroup": UrlUtil.PG_TABLES, "language": UrlUtil.PG_LANGUAGES, "recipe": UrlUtil.PG_RECIPES, "classFeature": UrlUtil.PG_CLASS_SUBCLASS_FEATURES, "subclassFeature": UrlUtil.PG_CLASS_SUBCLASS_FEATURES, "reference": UrlUtil.PG_QUICKREF, "referenceData": UrlUtil.PG_QUICKREF, "adventure": UrlUtil.PG_ADVENTURE, "adventureData": UrlUtil.PG_ADVENTURE, "book": UrlUtil.PG_BOOK, "bookData": UrlUtil.PG_BOOK, }; static _DATA_TYPE_LOADERS = {}; static _DATA_TYPE_LOADER_LIST = []; static _init () { this._registerPropToHashPages(); this._registerDataTypeLoaders(); return null; } static _registerPropToHashPages () { Object.entries(this._PROP_TO_HASH_PAGE) .forEach(([k, v]) => this._PROP_TO_HASH_PAGE[`${k}Fluff`] = _DataLoaderInternalUtil.getCleanPageFluff({page: v})); } static _registerDataTypeLoader ({loader, props, page, isFluff}) { this._DATA_TYPE_LOADER_LIST.push(loader); if (!props?.length) throw new Error(`No "props" specified for loader "${loader.constructor.name}"!`); props.forEach(prop => this._DATA_TYPE_LOADERS[_DataLoaderInternalUtil.getCleanPage({page: prop})] = loader); if (!page) return; this._DATA_TYPE_LOADERS[ isFluff ? _DataLoaderInternalUtil.getCleanPageFluff({page}) : _DataLoaderInternalUtil.getCleanPage({page}) ] = loader; } static _registerDataTypeLoaders () { const fnRegister = this._registerDataTypeLoader.bind(this); // region Multi-file _DataTypeLoaderCustomMonster.register({fnRegister}); _DataTypeLoaderCustomMonsterFluff.register({fnRegister}); _DataTypeLoaderCustomSpell.register({fnRegister}); _DataTypeLoaderCustomSpellFluff.register({fnRegister}); // endregion // region Predefined _DataTypeLoaderRace.register({fnRegister}); _DataTypeLoaderDeity.register({fnRegister}); _DataTypeLoaderVariantrule.register({fnRegister}); _DataTypeLoaderTable.register({fnRegister}); _DataTypeLoaderLanguage.register({fnRegister}); _DataTypeLoaderRecipe.register({fnRegister}); // endregion // region Special _DataTypeLoaderCustomClassesSubclass.register({fnRegister}); _DataTypeLoaderCustomClassSubclassFeature.register({fnRegister}); _DataTypeLoaderCustomItem.register({fnRegister}); _DataTypeLoaderCustomCard.register({fnRegister}); _DataTypeLoaderCustomDeck.register({fnRegister}); _DataTypeLoaderCustomQuickref.register({fnRegister}); _DataTypeLoaderCustomAdventure.register({fnRegister}); _DataTypeLoaderCustomBook.register({fnRegister}); // endregion // region Single file _DataTypeLoaderBackground.register({fnRegister}); _DataTypeLoaderPsionic.register({fnRegister}); _DataTypeLoaderObject.register({fnRegister}); _DataTypeLoaderAction.register({fnRegister}); _DataTypeLoaderFeat.register({fnRegister}); _DataTypeLoaderOptionalfeature.register({fnRegister}); _DataTypeLoaderReward.register({fnRegister}); _DataTypeLoaderCharoption.register({fnRegister}); _DataTypeLoaderTrapHazard.register({fnRegister}); _DataTypeLoaderCultBoon.register({fnRegister}); _DataTypeLoaderVehicle.register({fnRegister}); _DataTypeLoaderConditionDisease.register({fnRegister}); _DataTypeLoaderSkill.register({fnRegister}); _DataTypeLoaderSense.register({fnRegister}); _DataTypeLoaderLegendaryGroup.register({fnRegister}); _DataTypeLoaderItemEntry.register({fnRegister}); _DataTypeLoaderItemMastery.register({fnRegister}); _DataTypeLoaderCitation.register({fnRegister}); // endregion // region Fluff _DataTypeLoaderBackgroundFluff.register({fnRegister}); _DataTypeLoaderFeatFluff.register({fnRegister}); _DataTypeLoaderItemFluff.register({fnRegister}); _DataTypeLoaderRaceFluff.register({fnRegister}); _DataTypeLoaderLanguageFluff.register({fnRegister}); _DataTypeLoaderVehicleFluff.register({fnRegister}); _DataTypeLoaderObjectFluff.register({fnRegister}); _DataTypeLoaderCharoptionFluff.register({fnRegister}); _DataTypeLoaderRecipeFluff.register({fnRegister}); _DataTypeLoaderConditionDiseaseFluff.register({fnRegister}); _DataTypeLoaderTrapHazardFluff.register({fnRegister}); // endregion } static _ = this._init(); static _CACHE = new _DataLoaderCache(); static _LOCK_1 = new VeLock({isDbg: false, name: "loader-lock-1"}); static _LOCK_2 = new VeLock({isDbg: false, name: "loader-lock-2"}); /* -------------------------------------------- */ /** * @param page * @param source * @param hash * @param [isCopy] If a copy, rather than the original entity, should be returned. * @param [isRequired] If an error should be thrown on a missing entity. * @param [_isReturnSentinel] If a null sentinel should be returned, if it exists. * @param [_isInsertSentinelOnMiss] If a null sentinel should be inserted on cache miss. */ static getFromCache ( page, source, hash, { isCopy = false, isRequired = false, _isReturnSentinel = false, _isInsertSentinelOnMiss = false, } = {}, ) { const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({page, source, hash}); const ent = this._getFromCache({pageClean, sourceClean, hashClean, isCopy, _isReturnSentinel, _isInsertSentinelOnMiss}); return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent, isRequired}); } static _getFromCache ( { pageClean, sourceClean, hashClean, isCopy = false, _isInsertSentinelOnMiss = false, _isReturnSentinel = false, }, ) { const out = this._CACHE.get(pageClean, sourceClean, hashClean); if (out === _DataLoaderConst.ENTITY_NULL) { if (_isReturnSentinel) return out; if (!_isReturnSentinel) return null; } if (out == null && _isInsertSentinelOnMiss) { this._CACHE.set(pageClean, sourceClean, hashClean, _DataLoaderConst.ENTITY_NULL); } if (!isCopy || out == null) return out; return MiscUtil.copyFast(out); } /* -------------------------------------------- */ static _getVerifiedRequiredEntity ({pageClean, sourceClean, hashClean, ent, isRequired}) { if (ent || !isRequired) return ent; throw new Error(`Could not find entity for page/prop "${pageClean}" with source "${sourceClean}" and hash "${hashClean}"`); } /* -------------------------------------------- */ static async pCacheAndGetAllSite (page, {isSilent = false} = {}) { const pageClean = _DataLoaderInternalUtil.getCleanPage({page}); if (this._PAGES_NO_CONTENT.has(pageClean)) return null; const dataLoader = this._pCache_getDataTypeLoader({pageClean, isSilent}); if (!dataLoader) return null; // (Avoid preloading missing brew here, as we only return site data.) const {siteData} = await this._pCacheAndGet_getCacheMeta({pageClean, sourceClean: _DataLoaderConst.SOURCE_SITE_ALL, dataLoader}); await this._pCacheAndGet_processCacheMeta({dataLoader, siteData}); return this._CACHE.getAllSite(pageClean); } static async pCacheAndGetAllPrerelease (page, {isSilent = false} = {}) { return this._CacheAndGetAllPrerelease.pCacheAndGetAll({parent: this, page, isSilent}); } static async pCacheAndGetAllBrew (page, {isSilent = false} = {}) { return this._CacheAndGetAllBrew.pCacheAndGetAll({parent: this, page, isSilent}); } static _CacheAndGetAllPrereleaseBrew = class { static _SOURCE_ALL; static _PROP_DATA; static async pCacheAndGetAll ( { parent, page, isSilent, }, ) { const pageClean = _DataLoaderInternalUtil.getCleanPage({page}); if (parent._PAGES_NO_CONTENT.has(pageClean)) return null; const dataLoader = parent._pCache_getDataTypeLoader({pageClean, isSilent}); if (!dataLoader) return null; // (Avoid preloading missing prerelease/homebrew here, as we only return currently-loaded prerelease/homebrew.) const cacheMeta = await parent._pCacheAndGet_getCacheMeta({pageClean, sourceClean: this._SOURCE_ALL, dataLoader}); await parent._pCacheAndGet_processCacheMeta({dataLoader, [this._PROP_DATA]: cacheMeta[this._PROP_DATA]}); return this._getAllCached({parent, pageClean}); } /** @abstract */ static _getAllCached ({parent, pageClean}) { throw new Error("Unimplemented!"); } }; static _CacheAndGetAllPrerelease = class extends this._CacheAndGetAllPrereleaseBrew { static _SOURCE_ALL = _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT; static _PROP_DATA = "prereleaseData"; static _getAllCached ({parent, pageClean}) { return parent._CACHE.getAllPrerelease(pageClean); } }; static _CacheAndGetAllBrew = class extends this._CacheAndGetAllPrereleaseBrew { static _SOURCE_ALL = _DataLoaderConst.SOURCE_BREW_ALL_CURRENT; static _PROP_DATA = "brewData"; static _getAllCached ({parent, pageClean}) { return parent._CACHE.getAllBrew(pageClean); } }; /* -------------------------------------------- */ static _PAGES_NO_CONTENT = new Set([ _DataLoaderInternalUtil.getCleanPage({page: "generic"}), _DataLoaderInternalUtil.getCleanPage({page: "hover"}), ]); /** * @param page * @param source * @param hash * @param [isCopy] If a copy, rather than the original entity, should be returned. * @param [isRequired] If an error should be thrown on a missing entity. * @param [isSilent] If errors should not be thrown on a missing implementation. * @param [lockToken2] Post-process lock token for recursive calls. */ static async pCacheAndGet (page, source, hash, {isCopy = false, isRequired = false, isSilent = false, lockToken2} = {}) { const fromCache = this.getFromCache(page, source, hash, {isCopy, _isReturnSentinel: true}); if (fromCache === _DataLoaderConst.ENTITY_NULL) return null; if (fromCache) return fromCache; const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({page, source, hash}); if (this._PAGES_NO_CONTENT.has(pageClean)) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); const dataLoader = this._pCache_getDataTypeLoader({pageClean, isSilent}); if (!dataLoader) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); const isUnavailablePrerelease = await this._PrereleasePreloader._pPreloadMissing({parent: this, sourceClean}); if (isUnavailablePrerelease) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); const isUnavailableBrew = await this._BrewPreloader._pPreloadMissing({parent: this, sourceClean}); if (isUnavailableBrew) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); const {siteData = null, prereleaseData = null, brewData = null} = await this._pCacheAndGet_getCacheMeta({pageClean, sourceClean, dataLoader}); await this._pCacheAndGet_processCacheMeta({dataLoader, siteData, prereleaseData, brewData, lockToken2}); return this.getFromCache(page, source, hash, {isCopy, _isInsertSentinelOnMiss: true}); } static async pCacheAndGetHash (page, hash, opts) { const source = UrlUtil.decodeHash(hash).last(); return DataLoader.pCacheAndGet(page, source, hash, opts); } static _PrereleaseBrewPreloader = class { static _LOCK_0; static _SOURCES_ATTEMPTED; /** Cache of clean (lowercase) source -> URL. */ static _CACHE_SOURCE_CLEAN_TO_URL; static _SOURCE_ALL; /** * Phase 0: check if prerelease/homebrew, and if so, check/load the source (if available). * Track failures (i.e., there is no available JSON for the source requested), and skip repeated failures. * This allows us to avoid an expensive mass re-cache, if a source which does not exist is requested for * loading multiple times. */ static async pPreloadMissing ({parent, sourceClean}) { try { await this._LOCK_0.pLock(); return (await this._pPreloadMissing({parent, sourceClean})); } finally { this._LOCK_0.unlock(); } } /** * @param parent * @param sourceClean * @return {Promise} `true` if the source does not exist and could not be loaded, false otherwise. */ static async _pPreloadMissing ({parent, sourceClean}) { if (this._isExistingMiss({parent, sourceClean})) return true; if (!this._isPossibleSource({parent, sourceClean})) return false; if (sourceClean === this._SOURCE_ALL) return false; const brewUtil = this._getBrewUtil(); if (!brewUtil) { this._setExistingMiss({parent, sourceClean}); return true; } if (brewUtil.hasSourceJson(sourceClean)) return false; const urlBrew = await this._pGetSourceUrl({parent, sourceClean}); if (!urlBrew) { this._setExistingMiss({parent, sourceClean}); return true; } await brewUtil.pAddBrewFromUrl(urlBrew); return false; } static _isExistingMiss ({sourceClean}) { return this._SOURCES_ATTEMPTED.has(sourceClean); } static _setExistingMiss ({sourceClean}) { this._SOURCES_ATTEMPTED.add(sourceClean); } /* -------------------------------------------- */ static async _pInitCacheSourceToUrl () { if (this._CACHE_SOURCE_CLEAN_TO_URL) return; const index = await this._pGetUrlIndex(); if (!index) return this._CACHE_SOURCE_CLEAN_TO_URL = {}; const brewUtil = this._getBrewUtil(); const urlRoot = await brewUtil.pGetCustomUrl(); this._CACHE_SOURCE_CLEAN_TO_URL = Object.entries(index) .mergeMap(([src, url]) => ({[_DataLoaderInternalUtil.getCleanSource({source: src})]: brewUtil.getFileUrl(url, urlRoot)})); } static async _pGetUrlIndex () { try { return (await this._pGetSourceIndex()); } catch (e) { setTimeout(() => { throw e; }); return null; } } static async _pGetSourceUrl ({sourceClean}) { await this._pInitCacheSourceToUrl(); return this._CACHE_SOURCE_CLEAN_TO_URL[sourceClean]; } /** @abstract */ static _isPossibleSource ({parent, sourceClean}) { throw new Error("Unimplemented!"); } /** @abstract */ static _getBrewUtil () { throw new Error("Unimplemented!"); } /** @abstract */ static _pGetSourceIndex () { throw new Error("Unimplemented!"); } }; static _PrereleasePreloader = class extends this._PrereleaseBrewPreloader { static _LOCK_0 = new VeLock({isDbg: false, name: "loader-lock-0--prerelease"}); static _SOURCE_ALL = _DataLoaderConst.SOURCE_BREW_ALL_CURRENT; static _SOURCES_ATTEMPTED = new Set(); static _CACHE_SOURCE_CLEAN_TO_URL = null; static _isPossibleSource ({parent, sourceClean}) { return parent._isPrereleaseSource({sourceClean}) && !Parser.SOURCE_JSON_TO_FULL[Parser.sourceJsonToJson(sourceClean)]; } static _getBrewUtil () { return typeof PrereleaseUtil !== "undefined" ? PrereleaseUtil : null; } static _pGetSourceIndex () { return DataUtil.prerelease.pLoadSourceIndex(); } }; static _BrewPreloader = class extends this._PrereleaseBrewPreloader { static _LOCK_0 = new VeLock({isDbg: false, name: "loader-lock-0--brew"}); static _SOURCE_ALL = _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT; static _SOURCES_ATTEMPTED = new Set(); static _CACHE_SOURCE_CLEAN_TO_URL = null; static _isPossibleSource ({parent, sourceClean}) { return !parent._isSiteSource({sourceClean}) && !parent._isPrereleaseSource({sourceClean}); } static _getBrewUtil () { return typeof BrewUtil2 !== "undefined" ? BrewUtil2 : null; } static _pGetSourceIndex () { return DataUtil.brew.pLoadSourceIndex(); } }; static async _pCacheAndGet_getCacheMeta ({pageClean, sourceClean, dataLoader}) { try { await this._LOCK_1.pLock(); return (await this._pCache({pageClean, sourceClean, dataLoader})); } finally { this._LOCK_1.unlock(); } } static async _pCache ({pageClean, sourceClean, dataLoader}) { // region Fetch from site data const siteData = await dataLoader.pGetSiteData({pageClean, sourceClean}); this._pCache_addToCache({allDataMerged: siteData, propAllowlist: dataLoader.phase1CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); // Always early-exit, regardless of whether the entity was found in the cache, if we know this is a site source if (this._isSiteSource({sourceClean})) return {siteData}; // endregion const out = {siteData}; // region Fetch from already-stored prerelease/brew data // As we have preloaded missing prerelease/brew earlier in the flow, we know that a prerelease/brew is either // present, or unavailable if (typeof PrereleaseUtil !== "undefined") { const prereleaseData = await dataLoader.pGetStoredPrereleaseData(); this._pCache_addToCache({allDataMerged: prereleaseData, propAllowlist: dataLoader.phase1CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); out.prereleaseData = prereleaseData; } if (typeof BrewUtil2 !== "undefined") { const brewData = await dataLoader.pGetStoredBrewData(); this._pCache_addToCache({allDataMerged: brewData, propAllowlist: dataLoader.phase1CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); out.brewData = brewData; } // endregion return out; } static async _pCacheAndGet_processCacheMeta ({dataLoader, siteData = null, prereleaseData = null, brewData = null, lockToken2 = null}) { if (!dataLoader.hasPhase2Cache) return; try { lockToken2 = await this._LOCK_2.pLock({token: lockToken2}); await this._pCacheAndGet_processCacheMeta_({dataLoader, siteData, prereleaseData, brewData, lockToken2}); } finally { this._LOCK_2.unlock(); } } static async _pCacheAndGet_processCacheMeta_ ({dataLoader, siteData = null, prereleaseData = null, brewData = null, lockToken2 = null}) { const {siteDataPostCache, prereleaseDataPostCache, brewDataPostCache} = await dataLoader.pGetPostCacheData({siteData, prereleaseData, brewData, lockToken2}); this._pCache_addToCache({allDataMerged: siteDataPostCache, propAllowlist: dataLoader.phase2CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); this._pCache_addToCache({allDataMerged: prereleaseDataPostCache, propAllowlist: dataLoader.phase2CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); this._pCache_addToCache({allDataMerged: brewDataPostCache, propAllowlist: dataLoader.phase2CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); } static _pCache_getDataTypeLoader ({pageClean, isSilent}) { const dataLoader = this._DATA_TYPE_LOADERS[pageClean]; if (!dataLoader && !isSilent) throw new Error(`No loading strategy found for page "${pageClean}"!`); return dataLoader; } static _pCache_addToCache ({allDataMerged, propAllowlist}) { if (!allDataMerged) return; allDataMerged = {...allDataMerged}; this._DATA_TYPE_LOADER_LIST .filter(loader => loader.hasCustomCacheStrategy({obj: allDataMerged})) .forEach(loader => { const propsToRemove = loader.addToCacheCustom({cache: this._CACHE, obj: allDataMerged}); propsToRemove.forEach(prop => delete allDataMerged[prop]); }); Object.keys(allDataMerged) .forEach(prop => { if (!propAllowlist.has(prop)) return; const arr = allDataMerged[prop]; if (!arr?.length || !(arr instanceof Array)) return; const hashBuilder = UrlUtil.URL_TO_HASH_BUILDER[prop]; if (!hashBuilder) return; arr.forEach(ent => { this._pCache_addEntityToCache({prop, hashBuilder, ent}); DataUtil.proxy.getVersions(prop, ent) .forEach(entVer => this._pCache_addEntityToCache({prop, hashBuilder, ent: entVer})); }); }); } static _pCache_addEntityToCache ({prop, hashBuilder, ent}) { ent.__prop = ent.__prop || prop; const page = this._PROP_TO_HASH_PAGE[prop]; const source = SourceUtil.getEntitySource(ent); // const hash = hashBuilder(ent); const {page: propClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({page: prop, source, hash}); const pageClean = page ? _DataLoaderInternalUtil.getCleanPage({page}) : null; this._CACHE.set(propClean, sourceClean, hashClean, ent); if (pageClean) this._CACHE.set(pageClean, sourceClean, hashClean, ent); } /* -------------------------------------------- */ static _CACHE_SITE_SOURCE_CLEAN = null; static _doBuildSourceCaches () { this._CACHE_SITE_SOURCE_CLEAN = this._CACHE_SITE_SOURCE_CLEAN || new Set(Object.keys(Parser.SOURCE_JSON_TO_FULL) .map(src => _DataLoaderInternalUtil.getCleanSource({source: src}))); } static _isSiteSource ({sourceClean}) { if (sourceClean === _DataLoaderConst.SOURCE_SITE_ALL) return true; if (sourceClean === _DataLoaderConst.SOURCE_BREW_ALL_CURRENT) return false; if (sourceClean === _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT) return false; this._doBuildSourceCaches(); return this._CACHE_SITE_SOURCE_CLEAN.has(sourceClean); } static _isPrereleaseSource ({sourceClean}) { if (sourceClean === _DataLoaderConst.SOURCE_SITE_ALL) return false; if (sourceClean === _DataLoaderConst.SOURCE_BREW_ALL_CURRENT) return false; if (sourceClean === _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT) return true; this._doBuildSourceCaches(); return sourceClean.startsWith(_DataLoaderInternalUtil.getCleanSource({source: Parser.SRC_UA_PREFIX})) || sourceClean.startsWith(_DataLoaderInternalUtil.getCleanSource({source: Parser.SRC_UA_ONE_PREFIX})); } /* -------------------------------------------- */ static getDiagnosticsSummary (diagnostics) { diagnostics = diagnostics.filter(Boolean); if (!diagnostics.length) return ""; const filenames = diagnostics .map(it => it.filename) .filter(Boolean) .unique() .sort(SortUtil.ascSortLower); if (!filenames.length) return ""; return `Filename${filenames.length === 1 ? " was" : "s were"}: ${filenames.map(it => `"${it}"`).join("; ")}.`; } } // endregion /* -------------------------------------------- */ // region Exports globalThis.DataLoader = DataLoader; // endregion