import { FederationHost, getRemoteEntry, getRemoteInfo, } from '@module-federation/runtime'; import { loadScript, ModuleInfo, ProviderModuleInfo, } from '@module-federation/sdk'; import { Remote } from '@module-federation/runtime/types'; import { getPrefetchId, compatGetPrefetchId } from './common/runtime-utils'; declare module '@module-federation/runtime' { export interface Federation { __PREFETCH__: { entryLoading: Record>; instance: Map; __PREFETCH_EXPORTS__: Record Promise>>; }; } } type PrefetchExports = Record; export interface DataPrefetchOptions { name: string; remote?: Remote; origin?: FederationHost; remoteSnapshot?: ModuleInfo; } export interface prefetchOptions { id: string; functionId?: string; cacheStrategy?: () => boolean; refetchParams?: any; } // @ts-ignore init global variable for test globalThis.__FEDERATION__ ??= {}; globalThis.__FEDERATION__.__PREFETCH__ ??= { entryLoading: {}, instance: new Map(), __PREFETCH_EXPORTS__: {}, }; export class MFDataPrefetch { public prefetchMemory: Map>; public recordOutdate: Record>; private _exports: Record; private _options: DataPrefetchOptions; constructor(options: DataPrefetchOptions) { this.prefetchMemory = new Map(); this.recordOutdate = {}; this._exports = {}; this._options = options; this.global.instance.set(options.name, this); } get global(): Record { return globalThis.__FEDERATION__.__PREFETCH__; } static getInstance(id: string): MFDataPrefetch | undefined { return globalThis.__FEDERATION__.__PREFETCH__.instance.get(id); } async loadEntry(entry: string | undefined): Promise { const { name, remoteSnapshot, remote, origin } = this._options; if (entry) { const { buildVersion, globalName } = remoteSnapshot as ProviderModuleInfo; const uniqueKey = globalName || `${name}:${buildVersion}`; if (!this.global.entryLoading[uniqueKey]) { this.global.entryLoading[uniqueKey] = loadScript(entry, {}); } return this.global.entryLoading[uniqueKey]; } else { const remoteInfo = getRemoteInfo(remote as Remote); const module = origin!.moduleCache.get(remoteInfo.name); return getRemoteEntry({ origin: origin!, remoteInfo, remoteEntryExports: module ? module.remoteEntryExports : undefined, }); } } getProjectExports() { if (Object.keys(this._exports).length > 0) { return this._exports; } const { name } = this._options; const exportsPromiseFn = globalThis.__FEDERATION__.__PREFETCH__.__PREFETCH_EXPORTS__?.[name]; const exportsPromise = typeof exportsPromiseFn === 'function' ? exportsPromiseFn() : Promise.resolve({}); const resolve = exportsPromise.then( (exports: Record> = {}) => { // Match prefetch based on the function name suffix so that other capabilities can be expanded later. // Not all functions should be directly identified as prefetch functions const memory: Record> = {}; Object.keys(exports).forEach((key) => { memory[key] = {}; const exportVal = exports[key]; Object.keys(exportVal).reduce( (memo: Record, current: string) => { if ( current.toLocaleLowerCase().endsWith('prefetch') || current.toLocaleLowerCase() === 'default' ) { memo[current] = exportVal[current]; } return memo; }, memory[key], ); }); this.memorizeExports(memory); }, ); return resolve; } memorizeExports(exports: Record): void { this._exports = exports; } getExposeExports(id: string): PrefetchExports { const prefetchId = getPrefetchId(id); const compatId = compatGetPrefetchId(id); const prefetchExports = this._exports[prefetchId] || (this._exports[compatId] as PrefetchExports); return prefetchExports || {}; } prefetch(prefetchOptions: prefetchOptions): any { const { id, functionId = 'default', refetchParams } = prefetchOptions; let prefetchResult; const prefetchId = getPrefetchId(id); const compatId = compatGetPrefetchId(id); const memorizeId = id + functionId; const memory = this.prefetchMemory.get(memorizeId); if (!this.checkOutdate(prefetchOptions) && memory) { return memory; } const prefetchExports = this._exports[prefetchId] || (this._exports[compatId] as PrefetchExports); if (!prefetchExports) { return; } const executePrefetch = prefetchExports[functionId]; if (typeof executePrefetch === 'function') { if (refetchParams) { prefetchResult = executePrefetch(refetchParams); } else { prefetchResult = executePrefetch(); } } else { throw new Error( `[Module Federation Data Prefetch]: No prefetch function called ${functionId} export in prefetch file`, ); } this.memorize(memorizeId, prefetchResult); return prefetchResult; } memorize(id: string, value: any): void { this.prefetchMemory.set(id, value); } markOutdate( markOptions: Omit, isOutdate: boolean, ): void { const { id, functionId = 'default' } = markOptions; if (!this.recordOutdate[id]) { this.recordOutdate[id] = {}; } this.recordOutdate[id][functionId] = isOutdate; } checkOutdate(outdateOptions: prefetchOptions): boolean { const { id, functionId = 'default', cacheStrategy } = outdateOptions; if (typeof cacheStrategy === 'function') { return cacheStrategy(); } if (!this.recordOutdate[id]) { this.recordOutdate[id] = {}; } if (this.recordOutdate[id][functionId]) { this.markOutdate( { id, functionId, }, false, ); return true; } else { return false; } } }