From 9fca2f32278bbcaf3caa66006dcacb72b77df0eb Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Wed, 19 Jun 2024 17:28:15 -0500 Subject: [PATCH] fix: class-scoped xml cache for recomposition (#1348) --- .../convertContext/recompositionFinalizer.ts | 115 ++++++++++-------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/src/convert/convertContext/recompositionFinalizer.ts b/src/convert/convertContext/recompositionFinalizer.ts index 3b256e16c7..8c63e2cfe9 100644 --- a/src/convert/convertContext/recompositionFinalizer.ts +++ b/src/convert/convertContext/recompositionFinalizer.ts @@ -20,8 +20,6 @@ import { ConvertTransactionFinalizer } from './transactionFinalizer'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr'); -const xmlCache = new Map(); - type RecompositionStateValue = { /** * Parent component that children are rolled up into @@ -36,7 +34,7 @@ type RecompositionStateValue = { type RecompositionState = Map; type RecompositionStateValueWithParent = RecompositionStateValue & { component: SourceComponent }; - +type XmlCache = Map; /** * Merges child components that share the same parent in the conversion pipeline into a single file. * @@ -45,26 +43,31 @@ type RecompositionStateValueWithParent = RecompositionStateValue & { component: */ export class RecompositionFinalizer extends ConvertTransactionFinalizer { public transactionState: RecompositionState = new Map(); + private xmlCache: XmlCache = new Map(); public async finalize(): Promise { return Promise.all( - [...this.transactionState.values()].filter(ensureStateValueWithParent).map(stateValueToWriterFormat) + [...this.transactionState.values()] + .filter(ensureStateValueWithParent) + .map(stateValueToWriterFormat(this.xmlCache)) ); } } -const stateValueToWriterFormat = async (stateValue: RecompositionStateValueWithParent): Promise => ({ - component: stateValue.component, - writeInfos: [ - { - source: new JsToXml(await recompose(stateValue)), - output: join( - stateValue.component.type.directoryName, - `${stateValue.component.fullName}.${stateValue.component.type.suffix}` - ), - }, - ], -}); +const stateValueToWriterFormat = + (cache: XmlCache) => + async (stateValue: RecompositionStateValueWithParent): Promise => ({ + component: stateValue.component, + writeInfos: [ + { + source: new JsToXml(await recompose(cache)(stateValue)), + output: join( + stateValue.component.type.directoryName, + `${stateValue.component.fullName}.${stateValue.component.type.suffix}` + ), + }, + ], + }); type ChildWithXml = { xmlContents: JsonMap; @@ -72,36 +75,40 @@ type ChildWithXml = { groupName: string; }; -const recompose = async (stateValue: RecompositionStateValueWithParent): Promise => { - await getXmlFromCache(stateValue.component); - - const childXmls = await Promise.all( - (stateValue.children?.toArray() ?? []).filter(ensureMetadataComponentWithParent).map( - async (child): Promise => ({ - cmp: child, - xmlContents: await getXmlFromCache(child), - groupName: getXmlElement(child.type), - }) - ) - ); +const recompose = + (cache: XmlCache) => + async (stateValue: RecompositionStateValueWithParent): Promise => { + await getXmlFromCache(cache)(stateValue.component); + + const childXmls = await Promise.all( + (stateValue.children?.toArray() ?? []).filter(ensureMetadataComponentWithParent).map( + async (child): Promise => ({ + cmp: child, + xmlContents: await getXmlFromCache(cache)(child), + groupName: getXmlElement(child.type), + }) + ) + ); - const parentXmlContents = { - [XML_NS_KEY]: XML_NS_URL, - ...(await getStartingXml(stateValue.component)), - // group them into an object of arrays by groupName, then merge into parent - ...toSortedGroups(childXmls), - }; + const parentXmlContents = { + [XML_NS_KEY]: XML_NS_URL, + ...(await getStartingXml(cache)(stateValue.component)), + // group them into an object of arrays by groupName, then merge into parent + ...toSortedGroups(childXmls), + }; - return { - [stateValue.component.type.name]: parentXmlContents, + return { + [stateValue.component.type.name]: parentXmlContents, + }; }; -}; /** @returns {} if StartEmpty, otherwise gets the parent xml */ -const getStartingXml = async (parent: SourceComponent): Promise => - parent.type.strategies?.recomposition === RecompositionStrategy.StartEmpty - ? {} - : unwrapAndOmitNS(parent.type.name)(await getXmlFromCache(parent)) ?? {}; +const getStartingXml = + (cache: XmlCache) => + async (parent: SourceComponent): Promise => + parent.type.strategies?.recomposition === RecompositionStrategy.StartEmpty + ? {} + : unwrapAndOmitNS(parent.type.name)(await getXmlFromCache(cache)(parent)) ?? {}; /** throw if the parent component isn't in the state entry */ const ensureStateValueWithParent = ( @@ -152,18 +159,20 @@ const toSortedGroups = (items: ChildWithXml[]): JsonMap => { }; /** wrapper around the xml cache. Handles the nonDecomposed "parse from parent" optimization */ -const getXmlFromCache = async (cmp: SourceComponent): Promise => { - if (!cmp.xml) return {}; - const key = `${cmp.xml}:${cmp.fullName}`; - if (!xmlCache.has(key)) { - const parsed = - cmp.parent?.type.strategies?.transformer === 'nonDecomposed' - ? cmp.parseFromParentXml({ [cmp.parent.type.name]: await getXmlFromCache(cmp.parent) }) - : unwrapAndOmitNS(cmp.type.name)(await cmp.parseXml()) ?? {}; - xmlCache.set(key, parsed); - } - return xmlCache.get(key) ?? {}; -}; +const getXmlFromCache = + (xmlCache: XmlCache) => + async (cmp: SourceComponent): Promise => { + if (!cmp.xml) return {}; + const key = `${cmp.xml}:${cmp.fullName}`; + if (!xmlCache.has(key)) { + const parsed = + cmp.parent?.type.strategies?.transformer === 'nonDecomposed' + ? cmp.parseFromParentXml({ [cmp.parent.type.name]: await getXmlFromCache(xmlCache)(cmp.parent) }) + : unwrapAndOmitNS(cmp.type.name)(await cmp.parseXml()) ?? {}; + xmlCache.set(key, parsed); + } + return xmlCache.get(key) ?? {}; + }; /** composed function, exported from module for test */ export const unwrapAndOmitNS =