Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: class-scoped xml cache for recomposition #1348

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 62 additions & 53 deletions src/convert/convertContext/recompositionFinalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import { ConvertTransactionFinalizer } from './transactionFinalizer';
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

const xmlCache = new Map<string, JsonMap>();

type RecompositionStateValue = {
/**
* Parent component that children are rolled up into
Expand All @@ -36,7 +34,7 @@ type RecompositionStateValue = {
type RecompositionState = Map<string, RecompositionStateValue>;

type RecompositionStateValueWithParent = RecompositionStateValue & { component: SourceComponent };

type XmlCache = Map<string, JsonMap>;
/**
* Merges child components that share the same parent in the conversion pipeline into a single file.
*
Expand All @@ -45,63 +43,72 @@ type RecompositionStateValueWithParent = RecompositionStateValue & { component:
*/
export class RecompositionFinalizer extends ConvertTransactionFinalizer<RecompositionState> {
public transactionState: RecompositionState = new Map<string, RecompositionStateValue>();
private xmlCache: XmlCache = new Map();

public async finalize(): Promise<WriterFormat[]> {
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<WriterFormat> => ({
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<WriterFormat> => ({
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;
cmp: SourceComponent;
groupName: string;
};

const recompose = async (stateValue: RecompositionStateValueWithParent): Promise<JsonMap> => {
await getXmlFromCache(stateValue.component);

const childXmls = await Promise.all(
(stateValue.children?.toArray() ?? []).filter(ensureMetadataComponentWithParent).map(
async (child): Promise<ChildWithXml> => ({
cmp: child,
xmlContents: await getXmlFromCache(child),
groupName: getXmlElement(child.type),
})
)
);
const recompose =
(cache: XmlCache) =>
async (stateValue: RecompositionStateValueWithParent): Promise<JsonMap> => {
await getXmlFromCache(cache)(stateValue.component);

const childXmls = await Promise.all(
(stateValue.children?.toArray() ?? []).filter(ensureMetadataComponentWithParent).map(
async (child): Promise<ChildWithXml> => ({
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<JsonMap> =>
parent.type.strategies?.recomposition === RecompositionStrategy.StartEmpty
? {}
: unwrapAndOmitNS(parent.type.name)(await getXmlFromCache(parent)) ?? {};
const getStartingXml =
(cache: XmlCache) =>
async (parent: SourceComponent): Promise<JsonMap> =>
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 = (
Expand Down Expand Up @@ -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<JsonMap> => {
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<JsonMap> => {
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 =
Expand Down
Loading