@salesforce/lds-runtime-aura 1.314.0 → 1.316.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/ldsEngineCreator.js +2096 -1917
  2. package/dist/types/main.d.ts +18 -1
  3. package/dist/types/predictive-loading/lex/index.d.ts +1 -0
  4. package/dist/types/predictive-loading/lex/predictions-ready-manager.d.ts +32 -0
  5. package/dist/types/predictive-loading/pages/lex-default-page.d.ts +1 -0
  6. package/dist/types/predictive-loading/pages/object-home-page.d.ts +5 -3
  7. package/dist/types/predictive-loading/pages/predictive-prefetch-page.d.ts +1 -0
  8. package/dist/types/predictive-loading/pages/record-home-page.d.ts +5 -3
  9. package/dist/types/predictive-loading/prefetcher/lex-predictive-prefetcher.d.ts +3 -3
  10. package/dist/types/predictive-loading/prefetcher/predictive-prefetcher.d.ts +1 -1
  11. package/dist/types/predictive-loading/request-runner/lex-request-runner.d.ts +3 -4
  12. package/dist/types/predictive-loading/request-strategy/get-apex-request-strategy.d.ts +1 -0
  13. package/dist/types/predictive-loading/request-strategy/get-components-request-strategy.d.ts +1 -0
  14. package/dist/types/predictive-loading/request-strategy/get-object-info-request-strategy.d.ts +1 -0
  15. package/dist/types/predictive-loading/request-strategy/get-record-actions-request-strategy.d.ts +1 -0
  16. package/dist/types/predictive-loading/request-strategy/get-record-avatars-request-strategy.d.ts +1 -0
  17. package/dist/types/predictive-loading/request-strategy/get-record-request-strategy.d.ts +1 -0
  18. package/dist/types/predictive-loading/request-strategy/get-records-request-strategy.d.ts +1 -0
  19. package/dist/types/predictive-loading/request-strategy/get-related-list-info-request-strategy.d.ts +1 -0
  20. package/dist/types/predictive-loading/request-strategy/get-related-list-records-request-strategy.d.ts +1 -0
  21. package/dist/types/predictive-loading/request-strategy/get-related-lists-actions-request-strategy.d.ts +1 -0
  22. package/dist/types/predictive-loading/request-strategy-manager/request-strategy-manager.d.ts +12 -0
  23. package/dist/types/predictive-loading/storage/aura-prefetch-storage.d.ts +4 -2
  24. package/package.json +13 -13
@@ -21,7 +21,7 @@ import useCmpDefPredictions from '@salesforce/gate/lds.pdl.useCmpDefPredictions'
21
21
  import applyPredictionRequestLimit from '@salesforce/gate/lds.pdl.applyRequestLimit';
22
22
  import useExactMatchesPlusGate from '@salesforce/gate/lds.pdl.useExactMatchesPlus';
23
23
  import { GetApexWireAdapterFactory, registerPrefetcher as registerPrefetcher$1 } from 'force/ldsAdaptersApex';
24
- import { instrument, getRecordAvatarsAdapterFactory, getRecordAdapterFactory, coerceFieldIdArray, getRecordsAdapterFactory, getRecordActionsAdapterFactory, getObjectInfosAdapterFactory, coerceObjectIdArray, getObjectInfoAdapterFactory, coerceObjectId, getRelatedListsActionsAdapterFactory, getRelatedListInfoBatchAdapterFactory, getRelatedListInfoAdapterFactory, getRelatedListRecordsBatchAdapterFactory, getRelatedListRecordsAdapterFactory, getListInfoByNameAdapterFactory, getListInfosByObjectNameAdapterFactory, getListRecordsByNameAdapterFactory, getListObjectInfoAdapterFactory, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, registerPrefetcher } from 'force/ldsAdaptersUiapi';
24
+ import { getRecordAvatarsAdapterFactory, getRecordAdapterFactory, coerceFieldIdArray, getRecordsAdapterFactory, getRecordActionsAdapterFactory, getObjectInfosAdapterFactory, coerceObjectIdArray, getObjectInfoAdapterFactory, coerceObjectId, getRelatedListsActionsAdapterFactory, getRelatedListInfoBatchAdapterFactory, getRelatedListInfoAdapterFactory, getRelatedListRecordsBatchAdapterFactory, getRelatedListRecordsAdapterFactory, getListInfoByNameAdapterFactory, getListInfosByObjectNameAdapterFactory, getListRecordsByNameAdapterFactory, getListObjectInfoAdapterFactory, instrument, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, registerPrefetcher } from 'force/ldsAdaptersUiapi';
25
25
  import { BaseCommand, convertFetchResponseToData } from 'force/luvioRuntime5';
26
26
  import { serviceBroker } from 'force/luvioServiceBroker5';
27
27
  import oneStoreEnabled from '@salesforce/gate/lds.oneStoreEnabled.ltng';
@@ -1805,6 +1805,9 @@ class LexDefaultPage extends PredictivePrefetchPage {
1805
1805
  constructor(context) {
1806
1806
  super(context);
1807
1807
  }
1808
+ supportsRequest(_request) {
1809
+ return true;
1810
+ }
1808
1811
  buildSaveRequestData(request) {
1809
1812
  return [{ context: this.context, request }];
1810
1813
  }
@@ -1822,972 +1825,858 @@ class LexDefaultPage extends PredictivePrefetchPage {
1822
1825
  }
1823
1826
  }
1824
1827
 
1825
- class RecordHomePage extends LexDefaultPage {
1826
- constructor(context, requestStrategies, options) {
1827
- super(context);
1828
- this.requestStrategies = requestStrategies;
1829
- this.options = options;
1830
- const { recordId: _, ...rest } = this.context;
1831
- this.similarContext = {
1832
- recordId: '*',
1833
- ...rest,
1834
- };
1835
- }
1836
- buildSaveRequestData(request) {
1837
- const requestBuckets = [];
1838
- const { adapterName } = request;
1839
- const matchingRequestStrategy = this.requestStrategies.get(adapterName);
1840
- if (matchingRequestStrategy === undefined) {
1841
- return [];
1842
- }
1843
- if (matchingRequestStrategy.isContextDependent(this.context, request)) {
1844
- requestBuckets.push({
1845
- context: this.similarContext,
1846
- request: matchingRequestStrategy.transformForSaveSimilarRequest(request),
1847
- });
1848
- // When `options.useExactMatchesPlus` is not enabled, we can save this request on the similar bucket only
1849
- if (!this.options.useExactMatchesPlus) {
1850
- return requestBuckets;
1851
- }
1852
- }
1853
- if (!matchingRequestStrategy.onlySavedInSimilar) {
1854
- requestBuckets.push({
1855
- context: this.context,
1856
- request: matchingRequestStrategy.transformForSave(request),
1857
- });
1858
- }
1859
- return requestBuckets;
1860
- }
1861
- resolveSimilarRequest(similarRequest) {
1862
- const { adapterName } = similarRequest;
1863
- const matchingRequestStrategy = this.requestStrategies.get(adapterName);
1864
- if (matchingRequestStrategy === undefined) {
1865
- return similarRequest;
1866
- }
1867
- return matchingRequestStrategy.buildConcreteRequest(similarRequest, this.context);
1868
- }
1869
- // Record Home performs best when we always do a minimal getRecord alongside the other requests.
1870
- getAlwaysRunRequests() {
1871
- const { recordId, objectApiName } = this.context;
1872
- return [
1873
- {
1874
- adapterName: 'getRecord',
1875
- config: {
1876
- recordId,
1877
- optionalFields: [`${objectApiName}.Id`, `${objectApiName}.RecordTypeId`],
1878
- },
1879
- },
1880
- ];
1881
- }
1828
+ const { create, keys, hasOwnProperty, entries } = Object;
1829
+ const { isArray, from } = Array;
1830
+ const { stringify } = JSON;
1831
+
1832
+ class RequestStrategy {
1882
1833
  /**
1883
- * In RH, we know that there will be predictions, and we want to reduce the always requests (getRecord(id, type))
1884
- * with one of the predictions in case some request containing the fields was missed in the predictions.
1834
+ * Perform any transformations required to prepare the request for saving.
1885
1835
  *
1886
- * @returns true
1836
+ * e.g. If the request is for a record, we move all fields in the fields array
1837
+ * into the optionalFields array
1838
+ *
1839
+ * @param request - The request to transform
1840
+ * @returns
1887
1841
  */
1888
- shouldReduceAlwaysRequestsWithPredictions() {
1889
- return true;
1842
+ transformForSave(request) {
1843
+ return request;
1890
1844
  }
1891
1845
  /**
1892
- * In RH, we should execute the getRecord(id, type) by itself as we want the result asap, so
1893
- * it does not stop rendering.
1894
- * @returns true
1846
+ * Transforms the request for saving similar requests
1847
+ * @param request Request to transform for saving similar requests
1848
+ * @returns Transformed request
1895
1849
  */
1896
- shouldExecuteAlwaysRequestByThemself() {
1897
- return true;
1898
- }
1899
- static handlesContext(context) {
1900
- return (context !== undefined &&
1901
- context.actionName !== undefined &&
1902
- context.objectApiName !== undefined &&
1903
- context.recordId !== undefined &&
1904
- context.type === 'recordPage');
1850
+ transformForSaveSimilarRequest(request) {
1851
+ return this.transformForSave(request);
1905
1852
  }
1906
- }
1907
-
1908
- class ObjectHomePage extends LexDefaultPage {
1909
- constructor(context, requestStrategies, options) {
1910
- super(context);
1911
- this.requestStrategies = requestStrategies;
1912
- this.options = options;
1913
- this.similarContext = context;
1853
+ /**
1854
+ * Filter requests to only those that are for this strategy.
1855
+ *
1856
+ * @param unfilteredRequests array of requests to filter
1857
+ * @returns
1858
+ */
1859
+ filterRequests(unfilteredRequests) {
1860
+ return unfilteredRequests.filter((entry) => entry.request.adapterName === this.adapterName);
1914
1861
  }
1915
- buildSaveRequestData(request) {
1916
- const requestBuckets = [];
1917
- const { adapterName } = request;
1918
- const matchingRequestStrategy = this.requestStrategies.get(adapterName);
1919
- if (matchingRequestStrategy === undefined) {
1920
- return [];
1921
- }
1922
- if (matchingRequestStrategy.isContextDependent(this.context, request)) {
1923
- requestBuckets.push({
1924
- context: this.similarContext,
1925
- request: matchingRequestStrategy.transformForSaveSimilarRequest(request),
1926
- });
1927
- // When `options.useExactMatchesPlus` is not enabled, we can save this request on the similar bucket only
1928
- if (!this.options.useExactMatchesPlus) {
1929
- return requestBuckets;
1862
+ /**
1863
+ * Reduce requests by combining those based on a strategies implementations
1864
+ * of canCombine and combineRequests.
1865
+ * @param unfilteredRequests array of requests to filter
1866
+ * @returns
1867
+ */
1868
+ reduce(unfilteredRequests) {
1869
+ const requests = this.filterRequests(unfilteredRequests);
1870
+ const visitedRequests = new Set();
1871
+ const reducedRequests = [];
1872
+ for (let i = 0, n = requests.length; i < n; i++) {
1873
+ const currentRequest = requests[i];
1874
+ if (!visitedRequests.has(currentRequest)) {
1875
+ const combinedRequest = { ...currentRequest };
1876
+ for (let j = i + 1; j < n; j++) {
1877
+ const hasNotBeenVisited = !visitedRequests.has(requests[j]);
1878
+ const canCombineConfigs = this.canCombine(combinedRequest.request.config, requests[j].request.config);
1879
+ if (hasNotBeenVisited && canCombineConfigs) {
1880
+ combinedRequest.request.config = this.combineRequests(combinedRequest.request.config, requests[j].request.config);
1881
+ if (combinedRequest.requestMetadata.requestTime >
1882
+ requests[j].requestMetadata.requestTime) {
1883
+ // This logic is debateable - Currently this always assigns the lowest requestTime to a reduced request.
1884
+ combinedRequest.requestMetadata.requestTime =
1885
+ requests[j].requestMetadata.requestTime;
1886
+ }
1887
+ visitedRequests.add(requests[j]);
1888
+ }
1889
+ }
1890
+ reducedRequests.push(combinedRequest);
1891
+ visitedRequests.add(currentRequest);
1930
1892
  }
1931
1893
  }
1932
- requestBuckets.push({
1933
- context: this.context,
1934
- request: matchingRequestStrategy.transformForSave(request),
1935
- });
1936
- return requestBuckets;
1894
+ return reducedRequests;
1937
1895
  }
1938
- // no similar requests between LVs
1939
- resolveSimilarRequest(similarRequest) {
1940
- return similarRequest;
1896
+ /**
1897
+ * Check if two requests can be combined.
1898
+ *
1899
+ * By default, requests are not combinable.
1900
+ * @param reqA config of request A
1901
+ * @param reqB config of request B
1902
+ * @returns
1903
+ */
1904
+ canCombine(_reqA, _reqB) {
1905
+ return false;
1941
1906
  }
1942
- // these are requests that run always regardless of any other request existing
1943
- getAlwaysRunRequests() {
1944
- const { listViewApiName, objectApiName } = this.context;
1945
- return [
1946
- {
1947
- adapterName: 'getListInfoByName',
1948
- config: {
1949
- objectApiName: objectApiName,
1950
- listViewApiName: listViewApiName,
1951
- },
1952
- },
1953
- {
1954
- adapterName: 'getListInfosByObjectName',
1955
- config: {
1956
- objectApiName: objectApiName,
1957
- pageSize: 100,
1958
- q: '',
1959
- },
1960
- },
1961
- {
1962
- adapterName: 'getListInfosByObjectName',
1963
- config: {
1964
- objectApiName: objectApiName,
1965
- pageSize: 10,
1966
- recentListsOnly: true,
1967
- },
1968
- },
1969
- {
1970
- adapterName: 'getListObjectInfo',
1971
- config: {
1972
- objectApiName: objectApiName,
1973
- },
1974
- },
1975
- ];
1907
+ /**
1908
+ * Takes two request configs and combines them into a single request config.
1909
+ *
1910
+ * @param reqA config of request A
1911
+ * @param reqB config of request B
1912
+ * @returns
1913
+ */
1914
+ combineRequests(reqA, _reqB) {
1915
+ // By default, this should never be called since requests aren't combinable
1916
+ if (process.env.NODE_ENV !== 'production') {
1917
+ throw new Error('Not implemented');
1918
+ }
1919
+ return reqA;
1976
1920
  }
1977
1921
  /**
1978
- * AlwaysRequests must be reduced with predictions.
1922
+ * Checks adapter config against request context to determine if the request is context dependent.
1979
1923
  *
1980
- * @returns true
1924
+ * By default, requests are not context dependent.
1925
+ * @param request
1926
+ * @returns
1981
1927
  */
1982
- shouldReduceAlwaysRequestsWithPredictions() {
1983
- return true;
1928
+ isContextDependent(_context, _request) {
1929
+ return false;
1984
1930
  }
1985
1931
  /**
1986
- * In OH, the always requests are reduced with predictions, and because they
1987
- * can't be merged with other predictions, they will always run by themself.
1988
- * This value must be `false`, otherwise we may see repeated requests.
1932
+ * This tells PDL that requests of this strategy can only be saved in the similar bucket.
1989
1933
  *
1990
- * @returns false
1934
+ * @returns boolean
1991
1935
  */
1992
- shouldExecuteAlwaysRequestByThemself() {
1936
+ get onlySavedInSimilar() {
1993
1937
  return false;
1994
1938
  }
1995
- // Identifies a valid ObjectHomeContext
1996
- static handlesContext(context) {
1997
- return (context !== undefined &&
1998
- context.listViewApiName !== undefined &&
1999
- context.objectApiName !== undefined &&
2000
- context.type === 'objectHomePage');
1939
+ }
1940
+
1941
+ class LexRequestStrategy extends RequestStrategy {
1942
+ /**
1943
+ * Whether or not requests from this strategies can be boxcarred by Aura.
1944
+ * If they are, the lex prefetcher will run predictions of this strategy with a limit,
1945
+ * to avoid predictions being boxcared.
1946
+ *
1947
+ * @returns boolean
1948
+ */
1949
+ get isBoxcarable() {
1950
+ return true;
2001
1951
  }
2002
1952
  }
2003
1953
 
2004
- /**
2005
- * Observability / Critical Availability Program (230+)
2006
- *
2007
- * This file is intended to be used as a consolidated place for all definitions, functions,
2008
- * and helpers related to "M1"[1].
2009
- *
2010
- * Below are the R.E.A.D.S. metrics for the Lightning Data Service, defined here[2].
2011
- *
2012
- * [1] Search "[M1] Lightning Data Service Design Spike" in Quip
2013
- * [2] Search "Lightning Data Service R.E.A.D.S. Metrics" in Quip
2014
- */
2015
- const OBSERVABILITY_NAMESPACE = 'LIGHTNING.lds.service';
2016
- const ADAPTER_INVOCATION_COUNT_METRIC_NAME = 'request';
2017
- const ADAPTER_ERROR_COUNT_METRIC_NAME = 'error';
2018
- const NETWORK_ADAPTER_RESPONSE_METRIC_NAME = 'network-response';
2019
- /**
2020
- * W-8379680
2021
- * Counter for number of getApex requests.
2022
- */
2023
- const GET_APEX_REQUEST_COUNT = {
2024
- get() {
1954
+ const GET_COMPONENTS_DEF_ADAPTER_NAME = 'getComponentsDef';
1955
+ const noop = () => { };
1956
+ // Taken from https://sourcegraph.soma.salesforce.com/perforce.soma.salesforce.com/app/main/core/-/blob/ui-global-components/components/one/one/oneController.js?L75
1957
+ // In theory this should not be here, but for now, we don't have an alternative,
1958
+ // given these are started before o11y tells PDL the EPT windows ended (even before the timestamp sent by o11y).
1959
+ const onePreloads = new Set([
1960
+ 'markup://emailui:formattedEmailWrapper',
1961
+ 'markup://emailui:outputEmail',
1962
+ 'markup://flexipage:baseRecordHomeTemplateDesktop',
1963
+ 'markup://force:actionWindowLink',
1964
+ 'markup://force:inlineEditCell',
1965
+ 'markup://force:inputField',
1966
+ 'markup://force:recordPreviewItem',
1967
+ 'markup://force:relatedListDesktop',
1968
+ 'markup://force:relatedListQuickLinksContainer',
1969
+ 'markup://lst:relatedListQuickLinksContainer',
1970
+ 'markup://lst:secondDegreeRelatedListSingleContainer',
1971
+ 'markup://lst:bundle_act_coreListViewManagerDesktop',
1972
+ 'markup://lst:bundle_act_coreListViewManagerDesktop_generatedTemplates',
1973
+ 'markup://lst:baseFilterPanel',
1974
+ 'markup://lst:chartPanel',
1975
+ 'markup://force:socialPhotoWrapper',
1976
+ 'markup://forceContent:contentVersionsEditWizard',
1977
+ 'markup://forceContent:outputTitle',
1978
+ 'markup://forceContent:virtualRelatedListStencil',
1979
+ 'markup://forceSearch:resultsFilters',
1980
+ 'markup://interop:unstable_uiRecordApi',
1981
+ 'markup://lightning:formattedPhone',
1982
+ 'markup://notes:contentNoteRelatedListStencil',
1983
+ 'markup://one:alohaPage',
1984
+ 'markup://one:consoleObjectHome',
1985
+ 'markup://one:recordActionWrapper',
1986
+ 'markup://records:lwcDetailPanel',
1987
+ 'markup://records:lwcHighlightsPanel',
1988
+ 'markup://records:recordLayoutInputDateTime',
1989
+ 'markup://records:recordLayoutInputLocation',
1990
+ 'markup://records:recordLayoutItem',
1991
+ 'markup://records:recordLayoutLookup',
1992
+ 'markup://records:recordLayoutRichText',
1993
+ 'markup://records:recordLayoutRow',
1994
+ 'markup://records:recordLayoutSection',
1995
+ 'markup://records:recordLayoutTextArea',
1996
+ 'markup://records:recordPicklist',
1997
+ 'markup://sfa:outputNameWithHierarchyIcon',
1998
+ 'markup://runtime_platform_actions:actionHeadlessFormCancel',
1999
+ 'markup://runtime_platform_actions:actionHeadlessFormSave',
2000
+ 'markup://runtime_platform_actions:actionHeadlessFormSaveAndNew',
2001
+ 'markup://lightning:iconSvgTemplatesCustom',
2002
+ 'markup://lightning:iconSvgTemplatesDocType',
2003
+ 'markup://record_flexipage:recordHomeFlexipageUtil',
2004
+ 'markup://record_flexipage:recordFieldInstancesHandlers',
2005
+ 'markup://force:outputCustomLinkUrl',
2006
+ 'markup://force:quickActionRunnable',
2007
+ 'markup://force:inputURL',
2008
+ 'markup://force:inputMultiPicklist',
2009
+ 'markup://runtime_sales_activities:activityPanel',
2010
+ 'markup://support:outputLookupWithPreviewForSubject',
2011
+ 'markup://runtime_sales_activities:activitySubjectListView',
2012
+ 'markup://support:outputCaseSubjectField',
2013
+ 'markup://sfa:inputOpportunityAmount',
2014
+ 'markup://forceChatter:contentFileSize',
2015
+ 'markup://flexipage:column2',
2016
+ 'markup://sfa:outputNameWithHierarchyIconAccount',
2017
+ 'markup://emailui:formattedEmailAccount',
2018
+ 'markup://emailui:formattedEmailContact',
2019
+ 'markup://emailui:formattedEmailLead',
2020
+ 'markup://e.aura:serverActionError',
2021
+ 'markup://records:recordType',
2022
+ 'markup://flexipage:recordHomeWithSubheaderTemplateDesktop2',
2023
+ 'markup://force:customLinkUrl',
2024
+ 'markup://sfa:outputOpportunityAmount',
2025
+ 'markup://emailui:formattedEmailCase',
2026
+ 'markup://runtime_sales_activities:activitySubject',
2027
+ 'markup://lightning:quickActionAPI',
2028
+ 'markup://force:listViewManagerGridWrapText',
2029
+ 'markup://flexipage:recordHomeSimpleViewTemplate2',
2030
+ 'markup://flexipage:accordion2',
2031
+ 'markup://flexipage:accordionSection2',
2032
+ 'markup://flexipage:field',
2033
+ 'markup://runtime_iag_core:onboardingManager',
2034
+ 'markup://records:entityLabel',
2035
+ 'markup://records:highlightsHeaderRightContent',
2036
+ 'markup://records:formattedRichText',
2037
+ 'markup://force:socialRecordAvatarWrapper',
2038
+ 'markup://runtime_pipeline_inspector:pipelineInspectorHome',
2039
+ 'markup://sfa:inspectionDesktopObjectHome',
2040
+ 'markup://records:outputPhone',
2041
+ ]);
2042
+ function canPreloadDefinition(def) {
2043
+ return (def.startsWith('markup://') &&
2044
+ !(
2045
+ // some "virtual" components from flexipages are with `__` in the name, eg: design templates.
2046
+ // Not filtering out them will not cause errors, but will cause a server request that returns with error.
2047
+ (def.includes('__') ||
2048
+ // any generated template
2049
+ def.includes('forceGenerated') ||
2050
+ // part of onePreload
2051
+ def.includes('one:onePreloads') ||
2052
+ onePreloads.has(def))));
2053
+ }
2054
+ function requestComponents(config) {
2055
+ // Because { foo: undefined } can't be saved in indexedDB (serialization is {})
2056
+ // we need to manually save it as { "foo": "" }, and transform it to { foo: undefined }
2057
+ const descriptorsMap = {};
2058
+ let hasComponentsToLoad = false;
2059
+ for (const [def, uid] of entries(config)) {
2060
+ if (canPreloadDefinition(def)) {
2061
+ hasComponentsToLoad = true;
2062
+ descriptorsMap[def] = uid === '' ? undefined : uid;
2063
+ }
2064
+ }
2065
+ if (hasComponentsToLoad) {
2066
+ unstable_loadComponentDefs(descriptorsMap, noop);
2067
+ }
2068
+ }
2069
+ class GetComponentsDefStrategy extends LexRequestStrategy {
2070
+ constructor() {
2071
+ super(...arguments);
2072
+ this.adapterName = GET_COMPONENTS_DEF_ADAPTER_NAME;
2073
+ }
2074
+ execute(config) {
2075
+ return requestComponents(config);
2076
+ }
2077
+ buildConcreteRequest(similarRequest, _context) {
2025
2078
  return {
2026
- owner: OBSERVABILITY_NAMESPACE,
2027
- name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
2079
+ ...similarRequest,
2028
2080
  };
2029
- },
2030
- };
2031
- /**
2032
- * W-8828410
2033
- * Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
2034
- */
2035
- const TOTAL_ADAPTER_ERROR_COUNT = {
2036
- get() {
2037
- return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
2038
- },
2039
- };
2040
- /**
2041
- * W-8828410
2042
- * Counter for the number of invocations made into LDS by a wire adapter.
2043
- */
2044
- const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
2045
- get() {
2046
- return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
2047
- },
2048
- };
2049
-
2050
- const { create, keys, hasOwnProperty, entries } = Object;
2051
- const { isArray, from } = Array;
2052
- const { stringify } = JSON;
2053
-
2054
- /**
2055
- * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
2056
- * This is needed because insertion order for JSON.stringify(object) affects output:
2057
- * JSON.stringify({a: 1, b: 2})
2058
- * "{"a":1,"b":2}"
2059
- * JSON.stringify({b: 2, a: 1})
2060
- * "{"b":2,"a":1}"
2061
- * Modified from the apex implementation to sort arrays non-destructively.
2062
- * @param data Data to be JSON-stringified.
2063
- * @returns JSON.stringified value with consistent ordering of keys.
2064
- */
2065
- function stableJSONStringify$1(node) {
2066
- // This is for Date values.
2067
- if (node && node.toJSON && typeof node.toJSON === 'function') {
2068
- // eslint-disable-next-line no-param-reassign
2069
- node = node.toJSON();
2070
2081
  }
2071
- if (node === undefined) {
2072
- return;
2073
- }
2074
- if (typeof node === 'number') {
2075
- return isFinite(node) ? '' + node : 'null';
2082
+ transformForSave(request) {
2083
+ const normalizedConfig = {};
2084
+ for (const [def, uid] of entries(request.config || {})) {
2085
+ const normalizedDescriptorName = def.indexOf('://') === -1 ? 'markup://' + def : def;
2086
+ // uid can be a string, an object, or undefined.
2087
+ // when is an object or undefined, we can't say anything about the version,
2088
+ // and we can't save it as `undefined` as it can't be persisted to indexed db.
2089
+ normalizedConfig[normalizedDescriptorName] = typeof uid === 'string' ? uid : '';
2090
+ }
2091
+ return {
2092
+ ...request,
2093
+ config: normalizedConfig,
2094
+ };
2076
2095
  }
2077
- if (typeof node !== 'object') {
2078
- return stringify(node);
2096
+ canCombine() {
2097
+ return true;
2079
2098
  }
2080
- let i;
2081
- let out;
2082
- if (isArray(node)) {
2083
- // copy any array before sorting so we don't mutate the object.
2084
- // eslint-disable-next-line no-param-reassign
2085
- node = node.slice(0).sort();
2086
- out = '[';
2087
- for (i = 0; i < node.length; i++) {
2088
- if (i) {
2089
- out += ',';
2099
+ combineRequests(reqA, reqB) {
2100
+ const combinedDescriptors = {};
2101
+ // Note the order is important [reqA, reqB], reqB is always after reqA, and we want to keep the last seen uid
2102
+ // of a specific component.
2103
+ for (const descriptorMap of [reqA, reqB]) {
2104
+ for (const [def, uid] of entries(descriptorMap)) {
2105
+ if (canPreloadDefinition(def)) {
2106
+ combinedDescriptors[def] = uid;
2107
+ }
2090
2108
  }
2091
- out += stableJSONStringify$1(node[i]) || 'null';
2092
2109
  }
2093
- return out + ']';
2110
+ return combinedDescriptors;
2094
2111
  }
2095
- if (node === null) {
2096
- return 'null';
2112
+ get onlySavedInSimilar() {
2113
+ // Important: tells PDL to save this request only in the similar buckets.
2114
+ return true;
2097
2115
  }
2098
- const keys$1 = keys(node).sort();
2099
- out = '';
2100
- for (i = 0; i < keys$1.length; i++) {
2101
- const key = keys$1[i];
2102
- const value = stableJSONStringify$1(node[key]);
2103
- if (!value) {
2104
- continue;
2105
- }
2106
- if (out) {
2107
- out += ',';
2108
- }
2109
- out += stringify(key) + ':' + value;
2116
+ isContextDependent(_context, _request) {
2117
+ return true;
2118
+ }
2119
+ /**
2120
+ * Component predictions are not boxcared
2121
+ *
2122
+ * @returns false
2123
+ */
2124
+ get isBoxcarable() {
2125
+ return false;
2110
2126
  }
2111
- return '{' + out + '}';
2112
2127
  }
2113
- function isPromise(value) {
2114
- // check for Thenable due to test frameworks using custom Promise impls
2115
- return value !== null && value.then !== undefined;
2128
+
2129
+ const LDS_PDL_CMP_IDENTIFIER = 'lds:pdl';
2130
+ const DEFAULT_RESOURCE_CONTEXT = {
2131
+ sourceContext: {
2132
+ tagName: LDS_PDL_CMP_IDENTIFIER,
2133
+ actionConfig: {
2134
+ background: false,
2135
+ hotspot: true,
2136
+ longRunning: false,
2137
+ },
2138
+ },
2139
+ };
2140
+ class LuvioAdapterRequestStrategy extends LexRequestStrategy {
2141
+ constructor(luvio) {
2142
+ super();
2143
+ this.luvio = luvio;
2144
+ }
2145
+ execute(config, requestContext) {
2146
+ return this.adapterFactory(this.luvio)(config, {
2147
+ ...DEFAULT_RESOURCE_CONTEXT,
2148
+ ...requestContext,
2149
+ });
2150
+ }
2116
2151
  }
2117
2152
 
2118
- const APEX_ADAPTER_NAME = 'getApex';
2119
- const NORMALIZED_APEX_ADAPTER_NAME = `Apex.${APEX_ADAPTER_NAME}`;
2120
- const REFRESH_APEX_KEY = 'refreshApex';
2121
- const REFRESH_UIAPI_KEY = 'refreshUiApi';
2122
- const SUPPORTED_KEY = 'refreshSupported';
2123
- const UNSUPPORTED_KEY = 'refreshUnsupported';
2124
- const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
2125
- const REFRESH_EVENTTYPE = 'system';
2126
- const REFRESH_PAYLOAD_TARGET = 'adapters';
2127
- const REFRESH_PAYLOAD_SCOPE = 'lds';
2128
- const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
2129
- const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
2130
- const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
2131
- const NAMESPACE = 'lds';
2132
- const NETWORK_TRANSACTION_NAME = 'lds-network';
2133
- const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
2134
- // Aggregate Cache Stats and Metrics for all getApex invocations
2135
- const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
2136
- const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
2137
- // Observability (READS)
2138
- const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
2139
- const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
2140
- const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
2141
- class Instrumentation {
2153
+ const GET_RECORD_AVATARS_ADAPTER_NAME = 'getRecordAvatars';
2154
+ function normalizeRecordIds$1(recordIds) {
2155
+ if (!Array.isArray(recordIds)) {
2156
+ return [recordIds];
2157
+ }
2158
+ return recordIds;
2159
+ }
2160
+ class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
2142
2161
  constructor() {
2143
- this.adapterUnfulfilledErrorCounters = {};
2144
- this.recordApiNameChangeCounters = {};
2145
- this.refreshAdapterEvents = {};
2146
- this.refreshApiCallEventStats = {
2147
- [REFRESH_APEX_KEY]: 0,
2148
- [REFRESH_UIAPI_KEY]: 0,
2149
- [SUPPORTED_KEY]: 0,
2150
- [UNSUPPORTED_KEY]: 0,
2151
- };
2152
- this.lastRefreshApiCall = null;
2153
- this.weakEtagZeroEvents = {};
2154
- this.adapterCacheMisses = new LRUCache(250);
2155
- if (typeof window !== 'undefined' && window.addEventListener) {
2156
- window.addEventListener('beforeunload', () => {
2157
- if (keys(this.weakEtagZeroEvents).length > 0) {
2158
- perfStart(NETWORK_TRANSACTION_NAME);
2159
- perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
2160
- }
2161
- });
2162
- }
2163
- registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
2162
+ super(...arguments);
2163
+ this.adapterName = GET_RECORD_AVATARS_ADAPTER_NAME;
2164
+ this.adapterFactory = getRecordAvatarsAdapterFactory;
2164
2165
  }
2165
- /**
2166
- * Instruments an existing adapter to log argus metrics and cache stats.
2167
- * @param adapter The adapter function.
2168
- * @param metadata The adapter metadata.
2169
- * @param wireConfigKeyFn Optional function to transform wire configs to a unique key.
2170
- * @returns The wrapped adapter.
2171
- */
2172
- instrumentAdapter(adapter, metadata) {
2173
- // We are consolidating all apex adapter instrumentation calls under a single key
2174
- const { apiFamily, name, ttl } = metadata;
2175
- const adapterName = normalizeAdapterName(name, apiFamily);
2176
- const isGetApexAdapter = isApexAdapter(name);
2177
- const stats = isGetApexAdapter ? getApexCacheStats : registerLdsCacheStats(adapterName);
2178
- const ttlMissStats = isGetApexAdapter
2179
- ? getApexTtlCacheStats
2180
- : registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
2181
- /**
2182
- * W-8076905
2183
- * Dynamically generated metric. Simple counter for all requests made by this adapter.
2184
- */
2185
- const wireAdapterRequestMetric = isGetApexAdapter
2186
- ? getApexRequestCountMetric
2187
- : counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
2188
- const instrumentedAdapter = (config, requestContext) => {
2189
- // increment overall and adapter request metrics
2190
- wireAdapterRequestMetric.increment(1);
2191
- totalAdapterRequestSuccessMetric.increment(1);
2192
- // execute adapter logic
2193
- const result = adapter(config, requestContext);
2194
- // In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
2195
- // (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
2196
- //
2197
- // Note: we can't do a plain instanceof check for a promise here since the Promise may
2198
- // originate from another javascript realm (for example: in jest test). Instead we use a
2199
- // duck-typing approach by checking if the result has a then property.
2200
- //
2201
- // For adapters without persistent store:
2202
- // - total cache hit ratio:
2203
- // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
2204
- // For adapters with persistent store:
2205
- // - in-memory cache hit ratio:
2206
- // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
2207
- // - total cache hit ratio:
2208
- // ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
2209
- // if result === null then config is insufficient/invalid so do not log
2210
- if (isPromise(result)) {
2211
- stats.logMisses();
2212
- if (ttl !== undefined) {
2213
- this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
2214
- }
2215
- }
2216
- else if (result !== null) {
2217
- stats.logHits();
2218
- }
2219
- return result;
2166
+ buildConcreteRequest(similarRequest, context) {
2167
+ return {
2168
+ ...similarRequest,
2169
+ config: {
2170
+ ...similarRequest.config,
2171
+ recordIds: [context.recordId],
2172
+ },
2220
2173
  };
2221
- // Set the name property on the function for debugging purposes.
2222
- Object.defineProperty(instrumentedAdapter, 'name', {
2223
- value: name + '__instrumented',
2174
+ }
2175
+ transformForSaveSimilarRequest(request) {
2176
+ return this.transformForSave({
2177
+ ...request,
2178
+ config: {
2179
+ ...request.config,
2180
+ recordIds: ['*'],
2181
+ },
2224
2182
  });
2225
- return instrumentAdapter(instrumentedAdapter, metadata);
2226
2183
  }
2227
- /**
2228
- * Logs when adapter requests come in. If we have subsequent cache misses on a given config, beyond its TTL then log the duration to metrics.
2229
- * Backed by an LRU Cache implementation to prevent too many record entries from being stored in-memory.
2230
- * @param name The wire adapter name.
2231
- * @param config The config passed into wire adapter.
2232
- * @param ttlMissStats CacheStatsLogger to log misses out of TTL.
2233
- * @param currentCacheMissTimestamp Timestamp for when the request was made.
2234
- * @param ttl TTL for the wire adapter.
2235
- */
2236
- logAdapterCacheMissOutOfTtlDuration(name, config, ttlMissStats, currentCacheMissTimestamp, ttl) {
2237
- const configKey = `${name}:${stableJSONStringify$1(config)}`;
2238
- const existingCacheMissTimestamp = this.adapterCacheMisses.get(configKey);
2239
- this.adapterCacheMisses.set(configKey, currentCacheMissTimestamp);
2240
- if (existingCacheMissTimestamp !== undefined) {
2241
- const duration = currentCacheMissTimestamp - existingCacheMissTimestamp;
2242
- if (duration > ttl) {
2243
- ttlMissStats.logMisses();
2244
- }
2245
- }
2184
+ isContextDependent(context, request) {
2185
+ return (request.config.recordIds &&
2186
+ (context.recordId === request.config.recordIds || // some may set this as string instead of array
2187
+ (request.config.recordIds.length === 1 &&
2188
+ request.config.recordIds[0] === context.recordId)));
2246
2189
  }
2247
- /**
2248
- * Injected to LDS for Luvio specific instrumentation.
2249
- *
2250
- * @param context The transaction context.
2251
- */
2252
- instrumentLuvio(context) {
2253
- instrumentLuvio(context);
2254
- if (this.isRefreshAdapterEvent(context)) {
2255
- this.aggregateRefreshAdapterEvents(context);
2256
- }
2257
- else if (this.isAdapterUnfulfilledError(context)) {
2258
- this.incrementAdapterRequestErrorCount(context);
2259
- }
2260
- else ;
2190
+ canCombine(reqA, reqB) {
2191
+ return reqA.formFactor === reqB.formFactor;
2261
2192
  }
2262
- /**
2263
- * Returns whether or not this is a RefreshAdapterEvent.
2264
- * @param context The transaction context.
2265
- * @returns Whether or not this is a RefreshAdapterEvent.
2266
- */
2267
- isRefreshAdapterEvent(context) {
2268
- return context[REFRESH_ADAPTER_EVENT] === true;
2193
+ combineRequests(reqA, reqB) {
2194
+ const combined = { ...reqA };
2195
+ combined.recordIds = Array.from(new Set([...normalizeRecordIds$1(reqA.recordIds), ...normalizeRecordIds$1(reqB.recordIds)]));
2196
+ return combined;
2197
+ }
2198
+ }
2199
+
2200
+ const GET_RECORD_ADAPTER_NAME = 'getRecord';
2201
+ const COERCE_FIELD_ID_ARRAY_OPTIONS = { onlyQualifiedFieldNames: true };
2202
+ class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
2203
+ constructor() {
2204
+ super(...arguments);
2205
+ this.adapterName = GET_RECORD_ADAPTER_NAME;
2206
+ this.adapterFactory = getRecordAdapterFactory;
2269
2207
  }
2270
- /**
2271
- * Returns whether or not this is an AdapterUnfulfilledError.
2272
- * @param context The transaction context.
2273
- * @returns Whether or not this is an AdapterUnfulfilledError.
2274
- */
2275
- isAdapterUnfulfilledError(context) {
2276
- return context[ADAPTER_UNFULFILLED_ERROR] === true;
2208
+ buildConcreteRequest(similarRequest, context) {
2209
+ return {
2210
+ ...similarRequest,
2211
+ config: {
2212
+ ...similarRequest.config,
2213
+ recordId: context.recordId,
2214
+ },
2215
+ };
2277
2216
  }
2278
- /**
2279
- * Specific instrumentation for getRecordNotifyChange.
2280
- * temporary implementation to match existing aura call for now
2281
- *
2282
- * @param uniqueWeakEtags whether weakEtags match or not
2283
- * @param error if dispatchResourceRequest fails for any reason
2284
- */
2285
- notifyChangeNetwork(uniqueWeakEtags, error) {
2286
- perfStart(NETWORK_TRANSACTION_NAME);
2287
- if (error === true) {
2288
- perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
2289
- }
2290
- else {
2291
- perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
2217
+ transformForSave(request) {
2218
+ if (request.config.fields === undefined && request.config.optionalFields === undefined) {
2219
+ return request;
2292
2220
  }
2221
+ let fields = coerceFieldIdArray(request.config.fields, COERCE_FIELD_ID_ARRAY_OPTIONS) || [];
2222
+ let optionalFields = coerceFieldIdArray(request.config.optionalFields, COERCE_FIELD_ID_ARRAY_OPTIONS) || [];
2223
+ return {
2224
+ ...request,
2225
+ config: {
2226
+ ...request.config,
2227
+ fields: undefined,
2228
+ optionalFields: [...fields, ...optionalFields],
2229
+ },
2230
+ };
2293
2231
  }
2294
- /**
2295
- * Parses and aggregates weakETagZero events to be sent in summarized log line.
2296
- * @param context The transaction context.
2297
- */
2298
- aggregateWeakETagEvents(incomingWeakEtagZero, existingWeakEtagZero, apiName) {
2299
- const key = 'weaketag-0-' + apiName;
2300
- if (this.weakEtagZeroEvents[key] === undefined) {
2301
- this.weakEtagZeroEvents[key] = {
2302
- [EXISTING_WEAKETAG_0_KEY]: 0,
2303
- [INCOMING_WEAKETAG_0_KEY]: 0,
2304
- };
2305
- }
2306
- if (existingWeakEtagZero) {
2307
- this.weakEtagZeroEvents[key][EXISTING_WEAKETAG_0_KEY] += 1;
2308
- }
2309
- if (incomingWeakEtagZero) {
2310
- this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
2311
- }
2232
+ canCombine(reqA, reqB) {
2233
+ // must be same record and
2234
+ return (reqA.recordId === reqB.recordId &&
2235
+ // both requests are fields requests
2236
+ (reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
2237
+ (reqB.fields !== undefined || reqB.optionalFields !== undefined));
2312
2238
  }
2313
- /**
2314
- * Aggregates refresh adapter events to be sent in summarized log line.
2315
- * - how many times refreshApex is called
2316
- * - how many times refresh from lightning/uiRecordApi is called
2317
- * - number of supported calls: refreshApex called on apex adapter
2318
- * - number of unsupported calls: refreshApex on non-apex adapter
2319
- * + any use of refresh from uiRecordApi module
2320
- * - count of refresh calls per adapter
2321
- * @param context The refresh adapter event.
2322
- */
2323
- aggregateRefreshAdapterEvents(context) {
2324
- // We are consolidating all apex adapter instrumentation calls under a single key
2325
- // Adding additional logging that getApex adapters can invoke? Read normalizeAdapterName ts-doc.
2326
- const adapterName = normalizeAdapterName(context.adapterName);
2327
- if (this.lastRefreshApiCall === REFRESH_APEX_KEY) {
2328
- if (isApexAdapter(adapterName)) {
2329
- this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
2330
- }
2331
- else {
2332
- this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
2333
- }
2239
+ combineRequests(reqA, reqB) {
2240
+ const fields = new Set();
2241
+ const optionalFields = new Set();
2242
+ if (reqA.fields !== undefined) {
2243
+ reqA.fields.forEach((field) => fields.add(field));
2334
2244
  }
2335
- else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
2336
- this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
2245
+ if (reqB.fields !== undefined) {
2246
+ reqB.fields.forEach((field) => fields.add(field));
2337
2247
  }
2338
- if (this.refreshAdapterEvents[adapterName] === undefined) {
2339
- this.refreshAdapterEvents[adapterName] = 0;
2248
+ if (reqA.optionalFields !== undefined) {
2249
+ reqA.optionalFields.forEach((field) => optionalFields.add(field));
2340
2250
  }
2341
- this.refreshAdapterEvents[adapterName] += 1;
2342
- this.lastRefreshApiCall = null;
2343
- }
2344
- /**
2345
- * Increments call stat for incoming refresh api call, and sets the name
2346
- * to be used in {@link aggregateRefreshCalls}
2347
- * @param from The name of the refresh function called.
2348
- */
2349
- handleRefreshApiCall(apiName) {
2350
- this.refreshApiCallEventStats[apiName] += 1;
2351
- // set function call to be used with aggregateRefreshCalls
2352
- this.lastRefreshApiCall = apiName;
2353
- }
2354
- /**
2355
- * W-7302241
2356
- * Logs refresh call summary stats as a LightningInteraction.
2357
- */
2358
- logRefreshStats() {
2359
- if (keys(this.refreshAdapterEvents).length > 0) {
2360
- interaction(REFRESH_PAYLOAD_TARGET, REFRESH_PAYLOAD_SCOPE, this.refreshAdapterEvents, REFRESH_EVENTSOURCE, REFRESH_EVENTTYPE, this.refreshApiCallEventStats);
2361
- this.resetRefreshStats();
2251
+ if (reqB.optionalFields !== undefined) {
2252
+ reqB.optionalFields.forEach((field) => optionalFields.add(field));
2362
2253
  }
2363
- }
2364
- /**
2365
- * Resets the stat trackers for refresh call events.
2366
- */
2367
- resetRefreshStats() {
2368
- this.refreshAdapterEvents = {};
2369
- this.refreshApiCallEventStats = {
2370
- [REFRESH_APEX_KEY]: 0,
2371
- [REFRESH_UIAPI_KEY]: 0,
2372
- [SUPPORTED_KEY]: 0,
2373
- [UNSUPPORTED_KEY]: 0,
2254
+ return {
2255
+ recordId: reqA.recordId,
2256
+ fields: Array.from(fields),
2257
+ optionalFields: Array.from(optionalFields),
2374
2258
  };
2375
- this.lastRefreshApiCall = null;
2376
2259
  }
2377
- /**
2378
- * W-7801618
2379
- * Counter for occurrences where the incoming record to be merged has a different apiName.
2380
- * Dynamically generated metric, stored in an {@link RecordApiNameChangeCounters} object.
2381
- *
2382
- * @param context The transaction context.
2383
- *
2384
- * Note: Short-lived metric candidate, remove at the end of 230
2385
- */
2386
- incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
2387
- let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
2388
- if (apiNameChangeCounter === undefined) {
2389
- apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
2390
- this.recordApiNameChangeCounters[existingApiName] = apiNameChangeCounter;
2391
- }
2392
- apiNameChangeCounter.increment(1);
2260
+ isContextDependent(context, request) {
2261
+ return request.config.recordId === context.recordId;
2393
2262
  }
2394
- /**
2395
- * W-8620679
2396
- * Increment the counter for an UnfulfilledSnapshotError coming from luvio
2397
- *
2398
- * @param context The transaction context.
2399
- */
2400
- incrementAdapterRequestErrorCount(context) {
2401
- // We are consolidating all apex adapter instrumentation calls under a single key
2402
- const adapterName = normalizeAdapterName(context.adapterName);
2403
- let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
2404
- if (adapterRequestErrorCounter === undefined) {
2405
- adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
2406
- this.adapterUnfulfilledErrorCounters[adapterName] = adapterRequestErrorCounter;
2407
- }
2408
- adapterRequestErrorCounter.increment(1);
2409
- totalAdapterErrorMetric.increment(1);
2263
+ transformForSaveSimilarRequest(request) {
2264
+ return this.transformForSave({
2265
+ ...request,
2266
+ config: {
2267
+ ...request.config,
2268
+ recordId: '*',
2269
+ },
2270
+ });
2410
2271
  }
2411
2272
  }
2412
- function createMetricsKey(owner, name, unit) {
2413
- let metricName = name;
2414
- if (unit) {
2415
- metricName = metricName + '.' + unit;
2273
+
2274
+ const GET_RECORDS_ADAPTER_NAME = 'getRecords';
2275
+ class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
2276
+ constructor() {
2277
+ super(...arguments);
2278
+ this.adapterName = GET_RECORDS_ADAPTER_NAME;
2279
+ this.adapterFactory = getRecordsAdapterFactory;
2416
2280
  }
2417
- return {
2418
- get() {
2419
- return { owner: owner, name: metricName };
2420
- },
2421
- };
2422
- }
2423
- /**
2424
- * Returns whether adapter is an Apex one or not.
2425
- * @param adapterName The name of the adapter.
2426
- */
2427
- function isApexAdapter(adapterName) {
2428
- return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
2429
- }
2430
- /**
2431
- * Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
2432
- * API family, if supplied. Example: `UiApi.getRecord`.
2433
- *
2434
- * Note: If you are adding additional logging that can come from getApex adapter contexts that provide
2435
- * the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
2436
- * ensure to call this method to normalize all logging to 'getApex'. This
2437
- * is because Argus has a 50k key cardinality limit. More context: W-8379680.
2438
- *
2439
- * @param adapterName The name of the adapter.
2440
- * @param apiFamily The API family of the adapter.
2441
- */
2442
- function normalizeAdapterName(adapterName, apiFamily) {
2443
- if (isApexAdapter(adapterName)) {
2444
- return NORMALIZED_APEX_ADAPTER_NAME;
2281
+ buildConcreteRequest(similarRequest, context) {
2282
+ return {
2283
+ ...similarRequest,
2284
+ config: {
2285
+ ...similarRequest.config,
2286
+ records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
2287
+ },
2288
+ };
2445
2289
  }
2446
- return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
2447
- }
2448
- const timerMetricTracker = create(null);
2449
- /**
2450
- * Calls instrumentation/service telemetry timer
2451
- * @param name Name of the metric
2452
- * @param duration number to update backing percentile histogram, negative numbers ignored
2453
- */
2454
- function updateTimerMetric(name, duration) {
2455
- let metric = timerMetricTracker[name];
2456
- if (metric === undefined) {
2457
- metric = timer(createMetricsKey(NAMESPACE, name));
2458
- timerMetricTracker[name] = metric;
2290
+ isContextDependent(context, request) {
2291
+ const isSingleRecordRequest = request.config.records.length === 1 && request.config.records[0].recordIds.length === 1;
2292
+ return isSingleRecordRequest && request.config.records[0].recordIds[0] === context.recordId;
2293
+ }
2294
+ transformForSaveSimilarRequest(request) {
2295
+ return this.transformForSave({
2296
+ ...request,
2297
+ config: {
2298
+ ...request.config,
2299
+ records: [
2300
+ {
2301
+ ...request.config.records[0],
2302
+ recordIds: ['*'],
2303
+ },
2304
+ ],
2305
+ },
2306
+ });
2459
2307
  }
2460
- timerMetricAddDuration(metric, duration);
2461
2308
  }
2462
- function timerMetricAddDuration(timer, duration) {
2463
- // Guard against negative values since it causes error to be thrown by MetricsService
2464
- if (duration >= 0) {
2465
- timer.addDuration(duration);
2309
+
2310
+ const GET_RECORD_ACTIONS_ADAPTER_NAME = 'getRecordActions';
2311
+ function normalizeRecordIds(recordIds) {
2312
+ if (!isArray(recordIds)) {
2313
+ return [recordIds];
2466
2314
  }
2315
+ return recordIds;
2467
2316
  }
2468
- /**
2469
- * W-10315098
2470
- * Increments the counter associated with the request response. Counts are bucketed by status.
2471
- */
2472
- const requestResponseMetricTracker = create(null);
2473
- function incrementRequestResponseCount(cb) {
2474
- const status = cb().status;
2475
- let metric = requestResponseMetricTracker[status];
2476
- if (metric === undefined) {
2477
- metric = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, NETWORK_ADAPTER_RESPONSE_METRIC_NAME, `${status.valueOf()}`));
2478
- requestResponseMetricTracker[status] = metric;
2317
+ function normalizeApiNames(apiNames) {
2318
+ if (apiNames === undefined || apiNames === null) {
2319
+ return [];
2479
2320
  }
2480
- metric.increment();
2321
+ return isArray(apiNames) ? apiNames : [apiNames];
2481
2322
  }
2482
- function logObjectInfoChanged() {
2483
- logObjectInfoChanged$1();
2323
+ class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
2324
+ constructor() {
2325
+ super(...arguments);
2326
+ this.adapterName = GET_RECORD_ACTIONS_ADAPTER_NAME;
2327
+ this.adapterFactory = getRecordActionsAdapterFactory;
2328
+ }
2329
+ buildConcreteRequest(similarRequest, context) {
2330
+ return {
2331
+ ...similarRequest,
2332
+ config: {
2333
+ ...similarRequest.config,
2334
+ recordIds: [context.recordId],
2335
+ },
2336
+ };
2337
+ }
2338
+ transformForSaveSimilarRequest(request) {
2339
+ return this.transformForSave({
2340
+ ...request,
2341
+ config: {
2342
+ ...request.config,
2343
+ recordIds: ['*'],
2344
+ },
2345
+ });
2346
+ }
2347
+ canCombine(reqA, reqB) {
2348
+ return (reqA.retrievalMode === reqB.retrievalMode &&
2349
+ reqA.formFactor === reqB.formFactor &&
2350
+ (reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
2351
+ (reqA.sections || []).toString() === (reqB.sections || []).toString());
2352
+ }
2353
+ combineRequests(reqA, reqB) {
2354
+ const combined = { ...reqA };
2355
+ // let's merge the recordIds
2356
+ combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
2357
+ if (combined.retrievalMode === 'ALL') {
2358
+ const combinedSet = new Set([
2359
+ ...normalizeApiNames(combined.apiNames),
2360
+ ...normalizeApiNames(reqB.apiNames),
2361
+ ]);
2362
+ combined.apiNames = Array.from(combinedSet);
2363
+ }
2364
+ return combined;
2365
+ }
2366
+ isContextDependent(context, request) {
2367
+ return (request.config.recordIds &&
2368
+ (context.recordId === request.config.recordIds || // some may set this as string instead of array
2369
+ (request.config.recordIds.length === 1 &&
2370
+ request.config.recordIds[0] === context.recordId)));
2371
+ }
2484
2372
  }
2373
+
2374
+ const GET_OBJECT_INFO_BATCH_ADAPTER_NAME = 'getObjectInfos';
2485
2375
  /**
2486
- * Create a new instrumentation cache stats and return it.
2487
- *
2488
- * @param name The cache logger name.
2376
+ * Returns true if A is a superset of B
2377
+ * @param a
2378
+ * @param b
2489
2379
  */
2490
- function registerLdsCacheStats(name) {
2491
- return registerCacheStats(`${NAMESPACE}:${name}`);
2380
+ function isSuperSet(a, b) {
2381
+ return b.every((oan) => a.has(oan));
2492
2382
  }
2493
- /**
2494
- * Add or overwrite hooks that require aura implementations
2495
- */
2496
- function setAuraInstrumentationHooks() {
2497
- instrument({
2498
- recordConflictsResolved: (serverRequestCount) => {
2499
- // Ignore 0 values which can originate from ADS bridge
2500
- if (serverRequestCount > 0) {
2501
- updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
2502
- }
2503
- },
2504
- nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
2505
- const metricName = `merge-null-dv-count.${fieldType}`;
2506
- if (fieldType === 'scalar') {
2507
- incrementCounterMetric(`${metricName}.${areValuesEqual}`);
2383
+ class GetObjectInfosRequestStrategy extends LuvioAdapterRequestStrategy {
2384
+ constructor() {
2385
+ super(...arguments);
2386
+ this.adapterName = GET_OBJECT_INFO_BATCH_ADAPTER_NAME;
2387
+ this.adapterFactory = getObjectInfosAdapterFactory;
2388
+ }
2389
+ buildConcreteRequest(similarRequest) {
2390
+ return similarRequest;
2391
+ }
2392
+ transformForSave(request) {
2393
+ return {
2394
+ ...request,
2395
+ config: {
2396
+ ...request.config,
2397
+ // (!): if we are saving this request is because the adapter already verified is valid.
2398
+ objectApiNames: coerceObjectIdArray(request.config.objectApiNames),
2399
+ },
2400
+ };
2401
+ }
2402
+ /**
2403
+ * Reduces the given GetObjectInfosRequest requests by eliminating those for which config.objectApiNames
2404
+ * is a subset of another GetObjectInfosRequest.
2405
+ *
2406
+ * @param unfilteredRequests - Array of unfiltered requests
2407
+ * @returns RequestEntry<GetObjectInfosRequest>[] - Array of reduced requests
2408
+ */
2409
+ reduce(unfilteredRequests) {
2410
+ // Filter and sort requests by the length of objectApiNames in ascending order.
2411
+ // This ensures a superset of request (i) can only be found in a request (j) such that i < j.
2412
+ const objectInfosRequests = this.filterRequests(unfilteredRequests).sort((a, b) => a.request.config.objectApiNames.length - b.request.config.objectApiNames.length);
2413
+ // Convert request configurations to sets for easier comparison, avoiding a new set construction each iteration.
2414
+ const requestConfigAsSet = objectInfosRequests.map((r) => new Set(r.request.config.objectApiNames));
2415
+ const reducedRequests = [];
2416
+ // Iterate over each request to determine if it is a subset of others
2417
+ for (let i = 0, n = objectInfosRequests.length; i < n; i++) {
2418
+ const current = objectInfosRequests[i];
2419
+ const { request: { config: currentRequestConfig }, requestMetadata: currentRequestMetadata, } = current;
2420
+ let isCurrentSubsetOfOthers = false;
2421
+ // Check if the current request is a subset of any subsequent requests
2422
+ for (let j = i + 1; j < n; j++) {
2423
+ const possibleSuperset = objectInfosRequests[j];
2424
+ if (isSuperSet(requestConfigAsSet[j], currentRequestConfig.objectApiNames)) {
2425
+ isCurrentSubsetOfOthers = true;
2426
+ if (currentRequestMetadata.requestTime <
2427
+ possibleSuperset.requestMetadata.requestTime) {
2428
+ possibleSuperset.requestMetadata.requestTime =
2429
+ currentRequestMetadata.requestTime;
2430
+ }
2431
+ }
2508
2432
  }
2509
- else {
2510
- incrementCounterMetric(metricName);
2433
+ if (!isCurrentSubsetOfOthers) {
2434
+ reducedRequests.push(current);
2511
2435
  }
2512
- },
2513
- getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
2514
- getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
2515
- notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
2516
- notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
2517
- recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
2518
- weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
2519
- getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
2520
- });
2521
- withRegistration('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
2522
- instrument$1({
2523
- logCrud: logCRUDLightningInteraction,
2524
- networkResponse: incrementRequestResponseCount,
2525
- });
2526
- instrument$2({
2527
- error: logError,
2528
- });
2529
- instrument$3({
2530
- refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
2531
- instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
2532
- });
2533
- instrument$4({
2534
- timerMetricAddDuration: updateTimerMetric,
2535
- });
2536
- // Our getRecord through aggregate-ui CRUD logging has moved
2537
- // to lds-network-adapter. We still need to respect the
2538
- // orgs environment setting
2539
- if (forceRecordTransactionsDisabled$1 === false) {
2540
- ldsNetworkAdapterInstrument({
2541
- getRecordAggregateResolve: (cb) => {
2542
- const { recordId, apiName } = cb();
2543
- logCRUDLightningInteraction('read', {
2544
- recordId,
2545
- recordType: apiName,
2546
- state: 'SUCCESS',
2547
- });
2436
+ }
2437
+ return reducedRequests;
2438
+ }
2439
+ }
2440
+
2441
+ const GET_OBJECT_INFO_ADAPTER_NAME = 'getObjectInfo';
2442
+ class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
2443
+ constructor() {
2444
+ super(...arguments);
2445
+ this.adapterName = GET_OBJECT_INFO_ADAPTER_NAME;
2446
+ this.adapterFactory = getObjectInfoAdapterFactory;
2447
+ }
2448
+ buildConcreteRequest(similarRequest, context) {
2449
+ return {
2450
+ ...similarRequest,
2451
+ config: {
2452
+ ...similarRequest.config,
2453
+ objectApiName: context.objectApiName,
2548
2454
  },
2549
- getRecordAggregateReject: (cb) => {
2550
- const recordId = cb();
2551
- logCRUDLightningInteraction('read', {
2552
- recordId,
2553
- state: 'ERROR',
2554
- });
2455
+ };
2456
+ }
2457
+ transformForSave(request) {
2458
+ return {
2459
+ ...request,
2460
+ config: {
2461
+ ...request.config,
2462
+ // (!): if we are saving this request is because the adapter already verified is valid.
2463
+ objectApiName: coerceObjectId(request.config.objectApiName),
2555
2464
  },
2556
- });
2465
+ };
2466
+ }
2467
+ isContextDependent(context, request) {
2468
+ return (request.config.objectApiName !== undefined &&
2469
+ context.objectApiName === request.config.objectApiName);
2470
+ }
2471
+ /**
2472
+ * This method returns GetObjectInfoRequest[] that won't be part of a batch (getObjectInfos) request.
2473
+ *
2474
+ * @param unfilteredRequests all prediction requests
2475
+ * @returns
2476
+ */
2477
+ reduce(unfilteredRequests) {
2478
+ const objectApiNamesInBatchRequest = unfilteredRequests.filter((entry) => entry.request.adapterName === GET_OBJECT_INFO_BATCH_ADAPTER_NAME).reduce((acc, { request }) => {
2479
+ request.config.objectApiNames.forEach((apiName) => acc.add(apiName));
2480
+ return acc;
2481
+ }, new Set());
2482
+ const singleRequests = this.filterRequests(unfilteredRequests);
2483
+ return singleRequests.filter((singleEntry) => {
2484
+ return !objectApiNamesInBatchRequest.has(singleEntry.request.config.objectApiName);
2485
+ });
2557
2486
  }
2558
- withRegistration('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
2559
- }
2560
- /**
2561
- * Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
2562
- *
2563
- * @param luvio The Luvio instance to instrument.
2564
- * @param store The InMemoryStore to instrument.
2565
- */
2566
- function setupInstrumentation(luvio, store) {
2567
- setupInstrumentation$1(luvio, store);
2568
- setAuraInstrumentationHooks();
2569
- }
2570
- /**
2571
- * Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
2572
- * disable all crud operations if it is on.
2573
- * @param eventSource - Source of the logging event.
2574
- * @param attributes - Free form object of attributes to log.
2575
- */
2576
- function logCRUDLightningInteraction(eventSource, attributes) {
2577
- interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
2578
2487
  }
2579
- const instrumentation = new Instrumentation();
2580
2488
 
2581
- class ApplicationPredictivePrefetcher {
2582
- constructor(context, repository, requestRunner) {
2583
- this.repository = repository;
2584
- this.requestRunner = requestRunner;
2585
- this.isRecording = false;
2586
- this.totalRequestCount = 0;
2587
- this.queuedPredictionRequests = [];
2588
- this._context = context;
2589
- this.page = this.getPage();
2489
+ const GET_RELATED_LISTS_ACTIONS_ADAPTER_NAME = 'getRelatedListsActions';
2490
+ function isReduceAbleRelatedListConfig(config) {
2491
+ return config.relatedListsActionParameters.every((rlReq) => {
2492
+ return rlReq.relatedListId !== undefined && keys(rlReq).length === 1;
2493
+ });
2494
+ }
2495
+ class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy {
2496
+ constructor() {
2497
+ super(...arguments);
2498
+ this.adapterName = GET_RELATED_LISTS_ACTIONS_ADAPTER_NAME;
2499
+ this.adapterFactory = getRelatedListsActionsAdapterFactory;
2590
2500
  }
2591
- set context(value) {
2592
- this._context = value;
2593
- this.page = this.getPage();
2501
+ buildConcreteRequest(similarRequest, context) {
2502
+ return {
2503
+ ...similarRequest,
2504
+ config: {
2505
+ ...similarRequest.config,
2506
+ recordIds: [context.recordId],
2507
+ },
2508
+ };
2594
2509
  }
2595
- get context() {
2596
- return this._context;
2510
+ transformForSaveSimilarRequest(request) {
2511
+ return this.transformForSave({
2512
+ ...request,
2513
+ config: {
2514
+ ...request.config,
2515
+ recordIds: ['*'],
2516
+ },
2517
+ });
2597
2518
  }
2598
- async stopRecording() {
2599
- this.isRecording = false;
2600
- this.totalRequestCount = 0;
2601
- await this.repository.flushRequestsToStorage();
2519
+ isContextDependent(context, request) {
2520
+ const isForContext = request.config.recordIds &&
2521
+ (context.recordId === request.config.recordIds || // some may set this as string instead of array
2522
+ (request.config.recordIds.length === 1 &&
2523
+ request.config.recordIds[0] === context.recordId));
2524
+ return isForContext && isReduceAbleRelatedListConfig(request.config);
2602
2525
  }
2603
- startRecording() {
2604
- this.isRecording = true;
2605
- this.repository.markPageStart();
2606
- this.repository.clearRequestBuffer();
2526
+ /**
2527
+ * Can only reduce two requests when they have the same recordId, and
2528
+ * the individual relatedListAction config only have relatedListId.
2529
+ *
2530
+ * @param reqA
2531
+ * @param reqB
2532
+ * @returns boolean
2533
+ */
2534
+ canCombine(reqA, reqB) {
2535
+ const [recordIdA, recordIdB] = [reqA.recordIds, reqB.recordIds].map((recordIds) => {
2536
+ return isArray(recordIds)
2537
+ ? recordIds.length === 1
2538
+ ? recordIds[0]
2539
+ : null
2540
+ : recordIds;
2541
+ });
2542
+ return (recordIdA === recordIdB &&
2543
+ recordIdA !== null &&
2544
+ isReduceAbleRelatedListConfig(reqA) &&
2545
+ isReduceAbleRelatedListConfig(reqB));
2607
2546
  }
2608
- saveRequest(request) {
2609
- if (!this.isRecording) {
2610
- return;
2611
- }
2612
- executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_SAVE_REQUEST, (_act) => {
2613
- const saveBuckets = this.page.buildSaveRequestData(request);
2614
- saveBuckets.forEach((saveBucket) => {
2615
- const { request: requestToSave, context } = saveBucket;
2616
- // No need to differentiate from predictions requests because these
2617
- // are made from the adapters factory, which are not prediction aware.
2618
- this.repository.saveRequest(context, requestToSave);
2619
- });
2620
- return Promise.resolve().then();
2621
- }, PDL_EXECUTE_ASYNC_OPTIONS);
2547
+ combineRequests(reqA, reqB) {
2548
+ const relatedListsIncluded = new Set();
2549
+ [reqA, reqB].forEach(({ relatedListsActionParameters }) => {
2550
+ relatedListsActionParameters.forEach(({ relatedListId }) => relatedListsIncluded.add(relatedListId));
2551
+ });
2552
+ return {
2553
+ recordIds: reqA.recordIds,
2554
+ relatedListsActionParameters: from(relatedListsIncluded).map((relatedListId) => ({
2555
+ relatedListId,
2556
+ })),
2557
+ };
2622
2558
  }
2623
- async predict() {
2624
- const alwaysRequests = this.page.getAlwaysRunRequests();
2625
- const similarPageRequests = await this.getSimilarPageRequests();
2626
- const exactPageRequests = await this.getExactPageRequest();
2627
- // Always requests can't be reduced in - Some of them are essential to keep the page rendering at the beginning.
2628
- const reducedRequests = this.requestRunner
2629
- .reduceRequests([...exactPageRequests, ...similarPageRequests])
2630
- .map((entry) => entry.request);
2631
- const predictedRequests = [...alwaysRequests, ...reducedRequests];
2632
- this.queuedPredictionRequests.push(...predictedRequests);
2633
- this.totalRequestCount = predictedRequests.length;
2634
- return Promise.all(predictedRequests.map((request) => this.requestRunner.runRequest(request))).then();
2559
+ }
2560
+
2561
+ const GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME = 'getRelatedListInfoBatch';
2562
+ class GetRelatedListInfoBatchRequestStrategy extends LuvioAdapterRequestStrategy {
2563
+ constructor() {
2564
+ super(...arguments);
2565
+ this.adapterName = GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME;
2566
+ this.adapterFactory = getRelatedListInfoBatchAdapterFactory;
2635
2567
  }
2636
- getPredictionSummary() {
2637
- const exactPageRequests = this.repository.getPageRequests(this.context) || [];
2638
- const similarPageRequests = this.page.similarContext !== undefined
2639
- ? this.repository.getPageRequests(this.page.similarContext)
2640
- : [];
2568
+ buildConcreteRequest(similarRequest, _context) {
2569
+ return similarRequest;
2570
+ }
2571
+ /**
2572
+ * @override
2573
+ */
2574
+ transformForSave(request) {
2641
2575
  return {
2642
- exact: exactPageRequests.length,
2643
- similar: similarPageRequests.length,
2644
- totalRequestCount: this.totalRequestCount,
2576
+ ...request,
2577
+ config: {
2578
+ ...request.config,
2579
+ parentObjectApiName: coerceObjectId(request.config.parentObjectApiName),
2580
+ },
2645
2581
  };
2646
2582
  }
2647
- hasPredictions() {
2648
- const summary = this.getPredictionSummary();
2649
- return summary.exact > 0 || summary.similar > 0;
2650
- }
2651
- getSimilarPageRequests() {
2652
- let resolvedSimilarPageRequests = [];
2653
- if (this.page.similarContext !== undefined) {
2654
- const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
2655
- if (similarPageRequests !== undefined) {
2656
- resolvedSimilarPageRequests = similarPageRequests.map((entry) => {
2657
- return {
2658
- ...entry,
2659
- request: this.page.resolveSimilarRequest(entry.request),
2660
- };
2661
- });
2662
- }
2663
- }
2664
- return resolvedSimilarPageRequests;
2583
+ canCombine(reqA, reqB) {
2584
+ return reqA.parentObjectApiName === reqB.parentObjectApiName;
2665
2585
  }
2666
- getExactPageRequest() {
2667
- return this.repository.getPageRequests(this.context) || [];
2586
+ combineRequests(reqA, reqB) {
2587
+ const combined = { ...reqA };
2588
+ combined.relatedListNames = Array.from(new Set([...reqA.relatedListNames, ...reqB.relatedListNames]));
2589
+ return combined;
2668
2590
  }
2669
2591
  }
2670
2592
 
2671
- /**
2672
- * Runs a list of requests with a specified concurrency limit.
2673
- *
2674
- * @template Request - The type of the requests being processed.
2675
- * @param {RequestEntry<Request>[]} requests - An array of request entries to be processed in the array order.
2676
- * @param {RequestRunner<Request>} runner - The runner instance responsible for executing the requests.
2677
- * @param {number} concurrentRequestsLimit - The maximum number of concurrent requests allowed.
2678
- * @param {number} pageStartTime - The start time of the page load, used to calculate the time elapsed since the page starts loading.
2679
- * @returns {Promise<void>} A promise that resolves when all requests have been processed.
2680
- *
2681
- * This function manages a queue of pending requests and processes them with a concurrency limit.
2682
- * Requests are only processed if their `requestTime` is less than the time elapsed since `pageStartTime`.
2683
- */
2684
- async function runRequestsWithLimit(requests, runner, concurrentRequestsLimit, pageStartTime) {
2685
- // queue for pending prediction requests
2686
- const requestQueue = [...requests];
2687
- // Function to process the next request in the queue
2688
- const processNextRequest = async (verifyPastTime = true) => {
2689
- const timeInWaterfall = Date.now() - pageStartTime;
2690
- while (requestQueue.length > 0 &&
2691
- verifyPastTime &&
2692
- requestQueue[0].requestMetadata.requestTime <= timeInWaterfall) {
2693
- requestQueue.shift();
2694
- }
2695
- if (requestQueue.length > 0) {
2696
- // (!) requestQueue will always have at least one element ensured by the above check.
2697
- const nextRequest = requestQueue.shift();
2698
- try {
2699
- // Run the request and wait for it to complete
2700
- await runner.runRequest(nextRequest.request);
2701
- }
2702
- finally {
2703
- await processNextRequest();
2704
- }
2705
- }
2706
- };
2707
- // Start processing requests up to concurrentRequestsLimit
2708
- const initialRequests = Math.min(concurrentRequestsLimit, requestQueue.length);
2709
- const promises = [];
2710
- for (let i = 0; i < initialRequests; i++) {
2711
- // Initial requests should always execute, without verifying if they are past due.
2712
- // Reasoning:
2713
- // It may be that one of the alwaysRequest (with 0 as start time) that is reduced
2714
- // with the regular requests to make these to have 0 as the initial time in the waterfall.
2715
- // Because predictions are behind an await (see W-16139321), it could be that when this code is evaluated
2716
- // is already past time for the request.
2717
- promises.push(processNextRequest(false));
2593
+ const GET_RELATED_LIST_INFO_ADAPTER_NAME = 'getRelatedListInfo';
2594
+ class GetRelatedListInfoRequestStrategy extends LuvioAdapterRequestStrategy {
2595
+ constructor() {
2596
+ super(...arguments);
2597
+ this.adapterName = GET_RELATED_LIST_INFO_ADAPTER_NAME;
2598
+ this.adapterFactory = getRelatedListInfoAdapterFactory;
2718
2599
  }
2719
- // Wait for all initial requests to complete
2720
- await Promise.all(promises);
2721
- }
2722
-
2723
- function isBoxcarableRequest({ request: { adapterName } }, strategyMap) {
2724
- const strategy = strategyMap.get(adapterName);
2725
- return strategy === undefined || strategy.isBoxcarable;
2726
- }
2727
- function predictNonBoxcarableRequest(nonBoxcaredPredictions, requestRunner) {
2728
- const reducedPredictions = requestRunner.reduceRequests(nonBoxcaredPredictions);
2729
- reducedPredictions.map((request) => requestRunner.runRequest(request.request));
2730
- }
2731
- class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
2732
- constructor(context, repository, requestRunner,
2733
- // These strategies need to be in sync with the "predictiveDataLoadCapable" list
2734
- // from scripts/lds-uiapi-plugin.js
2735
- requestStrategies, options) {
2736
- super(context, repository, requestRunner);
2737
- this.options = options;
2738
- this.requestStrategyMap = new Map(requestStrategies.map((strategy) => [strategy.adapterName, strategy]));
2739
- this.page = this.getPage();
2600
+ /**
2601
+ * @override
2602
+ */
2603
+ isContextDependent(context, request) {
2604
+ // we have a higher degree of confidence of being able to use a similar request when
2605
+ // optional config is not set AND the object type of the page context is the same as the
2606
+ // object type related list is for
2607
+ return (context.objectApiName === request.config.parentObjectApiName &&
2608
+ this.isRequiredOnlyConfig(request.config));
2740
2609
  }
2741
- getPage() {
2742
- if (RecordHomePage.handlesContext(this.context)) {
2743
- return new RecordHomePage(this.context, this.requestStrategyMap, this.options);
2744
- }
2745
- else if (ObjectHomePage.handlesContext(this.context)) {
2746
- return new ObjectHomePage(this.context, this.requestStrategyMap, this.options);
2747
- }
2748
- return new LexDefaultPage(this.context);
2610
+ /**
2611
+ * @override
2612
+ */
2613
+ buildConcreteRequest(similarRequest, _context) {
2614
+ return similarRequest;
2749
2615
  }
2750
- getAllPageRequests() {
2751
- const exactPageRequests = this.getExactPageRequest();
2752
- let similarPageRequests = this.getSimilarPageRequests();
2753
- if (exactPageRequests.length > 0 && this.options.useExactMatchesPlus === true) {
2754
- similarPageRequests = similarPageRequests.filter((requestEntry) => {
2755
- const strategy = this.requestStrategyMap.get(requestEntry.request.adapterName);
2756
- return strategy && strategy.onlySavedInSimilar;
2757
- });
2758
- }
2759
- return [...exactPageRequests, ...similarPageRequests];
2616
+ /**
2617
+ * @override
2618
+ */
2619
+ transformForSave(request) {
2620
+ return {
2621
+ ...request,
2622
+ config: {
2623
+ ...request.config,
2624
+ parentObjectApiName: coerceObjectId(request.config.parentObjectApiName),
2625
+ },
2626
+ };
2760
2627
  }
2761
- async predict() {
2762
- const alwaysRequests = this.page.getAlwaysRunRequests();
2763
- const pageRequests = this.getAllPageRequests();
2764
- // IMPORTANT: Because there's no way to diferentiate a cmpDef prediction from the page
2765
- // requesting the cmpDef, we need to predict cmpDefs before we start watching
2766
- // for predictions in the page. Having this code after an
2767
- // await will make the predictions to be saved as predictions too.
2768
- predictNonBoxcarableRequest(pageRequests.filter((r) => !isBoxcarableRequest(r, this.requestStrategyMap)), this.requestRunner);
2769
- const alwaysRequestEntries = alwaysRequests.map((request) => {
2770
- return {
2771
- request,
2772
- requestMetadata: { requestTime: 0 }, // ensures always requests are executed, and executed first.
2773
- };
2628
+ /**
2629
+ * For performance reasons (fear of over-fetching), we only want to predict single requests which either are not
2630
+ * part of a batch request OR specify optional parameters (thus requiring data differing from that of a batch
2631
+ * request).
2632
+ *
2633
+ * ADG currently handles the batching of getRelatedListInfo -> getRelatedListInfoBatch
2634
+ * https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListInfoBatchReducer.js
2635
+ *
2636
+ * @param unfilteredRequests
2637
+ * @returns GetRelatedListInfoRequest[]
2638
+ * @override
2639
+ */
2640
+ reduce(unfilteredRequests) {
2641
+ // using batch versions of request, map keys of parent record to values of related lists
2642
+ const batchRequests = unfilteredRequests.filter((entry) => entry.request.adapterName === GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME).reduce((result, entry) => {
2643
+ // pull batch request parameters that correspond to single request parameters
2644
+ const { parentObjectApiName, relatedListNames, recordTypeId } = entry.request.config;
2645
+ // key based off of parent object and its type
2646
+ const key = `${parentObjectApiName}_${recordTypeId}`;
2647
+ // make sure an entry exists for the record
2648
+ const relatedListIds = result.get(key) || new Set();
2649
+ result.set(key, relatedListIds);
2650
+ // add each related list to values for the record
2651
+ relatedListNames.forEach((list) => relatedListIds.add(list));
2652
+ return result;
2653
+ }, new Map());
2654
+ // return only the single requests that will NOT be covered by a batch request
2655
+ return this.filterRequests(unfilteredRequests).filter((entry) => {
2656
+ const config = entry.request.config;
2657
+ if (!this.isRequiredOnlyConfig(config, true)) {
2658
+ // requested data is customized beyond default; batch may not cover, so keep single request
2659
+ return true;
2660
+ }
2661
+ // key based off of parent object and its type
2662
+ const key = `${config.parentObjectApiName}_${config.recordTypeId}`;
2663
+ // keep only the lists that are not batched
2664
+ const batchLists = batchRequests.get(key);
2665
+ return !(batchLists && batchLists.has(config.relatedListId));
2774
2666
  });
2775
- const boxcarablePredictions = pageRequests.filter((r) => isBoxcarableRequest(r, this.requestStrategyMap));
2776
- const reducedPredictions = this.page.shouldReduceAlwaysRequestsWithPredictions()
2777
- ? this.requestRunner.reduceRequests([...boxcarablePredictions, ...alwaysRequestEntries])
2778
- : this.requestRunner.reduceRequests(boxcarablePredictions);
2779
- const predictedRequestsWithLimit = (this.page.shouldExecuteAlwaysRequestByThemself()
2780
- ? [...alwaysRequestEntries, ...reducedPredictions]
2781
- : reducedPredictions)
2782
- // Sorting in order requested
2783
- .sort((a, b) => a.requestMetadata.requestTime - b.requestMetadata.requestTime);
2784
- this.totalRequestCount = predictedRequestsWithLimit.length;
2785
- await runRequestsWithLimit(predictedRequestsWithLimit, this.requestRunner, this.options.inflightRequestLimit,
2786
- // `this.repository.pageStartTime` would be the correct here,
2787
- // but when doing predict+watch, it could be (set in watch)
2788
- // repository.startTime is not set yet, Date.now() is a better alternative,
2789
- // that is correct, and works on both cases.
2790
- Date.now());
2667
+ }
2668
+ /**
2669
+ * Return true if the request config doesn't specify values for any of the optional parameters.
2670
+ *
2671
+ * @param config
2672
+ * @param ignoreRecordType
2673
+ */
2674
+ isRequiredOnlyConfig(config, ignoreRecordType = false) {
2675
+ return ((ignoreRecordType || config.recordTypeId === undefined) &&
2676
+ (config.fields || []).length === 0 &&
2677
+ (config.optionalFields || []).length === 0 &&
2678
+ config.restrictColumnsToLayout !== false // default is true
2679
+ );
2791
2680
  }
2792
2681
  }
2793
2682
 
@@ -2802,7 +2691,7 @@ class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
2802
2691
  * @param data Data to be JSON-stringified.
2803
2692
  * @returns JSON.stringified value with consistent ordering of keys.
2804
2693
  */
2805
- function stableJSONStringify(node) {
2694
+ function stableJSONStringify$1(node) {
2806
2695
  // This is for Date values.
2807
2696
  if (node && node.toJSON && typeof node.toJSON === 'function') {
2808
2697
  // eslint-disable-next-line no-param-reassign
@@ -2825,7 +2714,7 @@ function stableJSONStringify(node) {
2825
2714
  if (i) {
2826
2715
  out += ',';
2827
2716
  }
2828
- out += stableJSONStringify(node[i]) || 'null';
2717
+ out += stableJSONStringify$1(node[i]) || 'null';
2829
2718
  }
2830
2719
  return out + ']';
2831
2720
  }
@@ -2836,7 +2725,7 @@ function stableJSONStringify(node) {
2836
2725
  out = '';
2837
2726
  for (i = 0; i < keys$1.length; i++) {
2838
2727
  const key = keys$1[i];
2839
- const value = stableJSONStringify(node[key]);
2728
+ const value = stableJSONStringify$1(node[key]);
2840
2729
  if (!value) {
2841
2730
  continue;
2842
2731
  }
@@ -2867,1216 +2756,1369 @@ function deepEquals(objA, objB) {
2867
2756
  for (const key of keysA) {
2868
2757
  const valA = objA[key];
2869
2758
  const valB = objB[key];
2870
- const areObjects = isObject(valA) && isObject(valB);
2871
- // If both values are objects, recursively compare them
2872
- if (areObjects && !deepEquals(valA, valB))
2873
- return false;
2874
- // If only one value is an object or if the values are not strictly equal, they are not deeply equal
2875
- if (!areObjects && valA !== valB)
2876
- return false;
2877
- }
2878
- return true;
2879
- }
2880
-
2881
- class PrefetchRepository {
2882
- constructor(storage, options = {}) {
2883
- this.storage = storage;
2884
- this.options = options;
2885
- this.requestBuffer = new Map();
2886
- this.pageStartTime = Date.now();
2887
- }
2888
- clearRequestBuffer() {
2889
- this.requestBuffer.clear();
2890
- }
2891
- markPageStart() {
2892
- this.pageStartTime = Date.now();
2893
- }
2894
- async flushRequestsToStorage() {
2895
- const setPromises = [];
2896
- for (const [id, batch] of this.requestBuffer) {
2897
- const page = { id, requests: [] };
2898
- batch.forEach(({ request, requestTime }) => {
2899
- const existingRequestEntry = page.requests.find(({ request: storedRequest }) => deepEquals(storedRequest, request));
2900
- if (existingRequestEntry === undefined) {
2901
- page.requests.push({
2902
- request,
2903
- requestMetadata: {
2904
- requestTime,
2905
- },
2906
- });
2907
- }
2908
- else if (requestTime < existingRequestEntry.requestMetadata.requestTime) {
2909
- existingRequestEntry.requestMetadata.requestTime = requestTime;
2910
- }
2911
- });
2912
- const { modifyBeforeSaveHook } = this.options;
2913
- if (modifyBeforeSaveHook !== undefined) {
2914
- page.requests = modifyBeforeSaveHook(page.requests);
2915
- }
2916
- setPromises.push(this.storage.set(id, page));
2917
- }
2918
- this.clearRequestBuffer();
2919
- await Promise.all(setPromises);
2920
- }
2921
- getKeyId(key) {
2922
- return stableJSONStringify(key);
2923
- }
2924
- saveRequest(key, request) {
2925
- const identifier = this.getKeyId(key);
2926
- const batchForKey = this.requestBuffer.get(identifier) || [];
2927
- batchForKey.push({
2928
- request,
2929
- requestTime: Date.now() - this.pageStartTime,
2930
- });
2931
- this.requestBuffer.set(identifier, batchForKey);
2932
- }
2933
- getPage(key) {
2934
- const identifier = stableJSONStringify(key);
2935
- return this.storage.get(identifier);
2936
- }
2937
- getPageRequests(key) {
2938
- const page = this.getPage(key);
2939
- if (page === undefined) {
2940
- return [];
2941
- }
2942
- return page.requests;
2943
- }
2944
- }
2945
-
2946
- class LexRequestRunner {
2947
- constructor() {
2948
- this.requestStrategies = [];
2949
- this.requestStrategiesByAdapterName = new Map();
2950
- }
2951
- setRequestStrategies(strategies) {
2952
- this.requestStrategies = strategies;
2953
- this.requestStrategiesByAdapterName = new Map(strategies.map((strategy) => [strategy.adapterName, strategy]));
2954
- }
2955
- reduceRequests(requests) {
2956
- return this.requestStrategies.map((strategy) => strategy.reduce(requests)).flat();
2957
- }
2958
- runRequest(request) {
2959
- const strategy = this.requestStrategiesByAdapterName.get(request.adapterName);
2960
- if (strategy) {
2961
- return Promise.resolve(strategy.execute(request.config)).then();
2962
- }
2963
- return Promise.resolve(undefined);
2964
- }
2965
- }
2966
-
2967
- class RequestStrategy {
2968
- /**
2969
- * Perform any transformations required to prepare the request for saving.
2970
- *
2971
- * e.g. If the request is for a record, we move all fields in the fields array
2972
- * into the optionalFields array
2973
- *
2974
- * @param request - The request to transform
2975
- * @returns
2976
- */
2977
- transformForSave(request) {
2978
- return request;
2979
- }
2980
- /**
2981
- * Transforms the request for saving similar requests
2982
- * @param request Request to transform for saving similar requests
2983
- * @returns Transformed request
2984
- */
2985
- transformForSaveSimilarRequest(request) {
2986
- return this.transformForSave(request);
2987
- }
2988
- /**
2989
- * Filter requests to only those that are for this strategy.
2990
- *
2991
- * @param unfilteredRequests array of requests to filter
2992
- * @returns
2993
- */
2994
- filterRequests(unfilteredRequests) {
2995
- return unfilteredRequests.filter((entry) => entry.request.adapterName === this.adapterName);
2996
- }
2997
- /**
2998
- * Reduce requests by combining those based on a strategies implementations
2999
- * of canCombine and combineRequests.
3000
- * @param unfilteredRequests array of requests to filter
3001
- * @returns
3002
- */
3003
- reduce(unfilteredRequests) {
3004
- const requests = this.filterRequests(unfilteredRequests);
3005
- const visitedRequests = new Set();
3006
- const reducedRequests = [];
3007
- for (let i = 0, n = requests.length; i < n; i++) {
3008
- const currentRequest = requests[i];
3009
- if (!visitedRequests.has(currentRequest)) {
3010
- const combinedRequest = { ...currentRequest };
3011
- for (let j = i + 1; j < n; j++) {
3012
- const hasNotBeenVisited = !visitedRequests.has(requests[j]);
3013
- const canCombineConfigs = this.canCombine(combinedRequest.request.config, requests[j].request.config);
3014
- if (hasNotBeenVisited && canCombineConfigs) {
3015
- combinedRequest.request.config = this.combineRequests(combinedRequest.request.config, requests[j].request.config);
3016
- if (combinedRequest.requestMetadata.requestTime >
3017
- requests[j].requestMetadata.requestTime) {
3018
- // This logic is debateable - Currently this always assigns the lowest requestTime to a reduced request.
3019
- combinedRequest.requestMetadata.requestTime =
3020
- requests[j].requestMetadata.requestTime;
3021
- }
3022
- visitedRequests.add(requests[j]);
3023
- }
2759
+ const areObjects = isObject(valA) && isObject(valB);
2760
+ // If both values are objects, recursively compare them
2761
+ if (areObjects && !deepEquals(valA, valB))
2762
+ return false;
2763
+ // If only one value is an object or if the values are not strictly equal, they are not deeply equal
2764
+ if (!areObjects && valA !== valB)
2765
+ return false;
2766
+ }
2767
+ return true;
2768
+ }
2769
+
2770
+ class PrefetchRepository {
2771
+ constructor(storage, options = {}) {
2772
+ this.storage = storage;
2773
+ this.options = options;
2774
+ this.requestBuffer = new Map();
2775
+ this.pageStartTime = Date.now();
2776
+ }
2777
+ clearRequestBuffer() {
2778
+ this.requestBuffer.clear();
2779
+ }
2780
+ markPageStart() {
2781
+ this.pageStartTime = Date.now();
2782
+ }
2783
+ async flushRequestsToStorage() {
2784
+ const setPromises = [];
2785
+ for (const [id, batch] of this.requestBuffer) {
2786
+ const page = { id, requests: [] };
2787
+ batch.forEach(({ request, requestTime }) => {
2788
+ const existingRequestEntry = page.requests.find(({ request: storedRequest }) => deepEquals(storedRequest, request));
2789
+ if (existingRequestEntry === undefined) {
2790
+ page.requests.push({
2791
+ request,
2792
+ requestMetadata: {
2793
+ requestTime,
2794
+ },
2795
+ });
3024
2796
  }
3025
- reducedRequests.push(combinedRequest);
3026
- visitedRequests.add(currentRequest);
2797
+ else if (requestTime < existingRequestEntry.requestMetadata.requestTime) {
2798
+ existingRequestEntry.requestMetadata.requestTime = requestTime;
2799
+ }
2800
+ });
2801
+ const { modifyBeforeSaveHook } = this.options;
2802
+ if (modifyBeforeSaveHook !== undefined) {
2803
+ page.requests = modifyBeforeSaveHook(page.requests);
3027
2804
  }
2805
+ setPromises.push(this.storage.set(id, page));
3028
2806
  }
3029
- return reducedRequests;
2807
+ this.clearRequestBuffer();
2808
+ await Promise.all(setPromises);
3030
2809
  }
3031
- /**
3032
- * Check if two requests can be combined.
3033
- *
3034
- * By default, requests are not combinable.
3035
- * @param reqA config of request A
3036
- * @param reqB config of request B
3037
- * @returns
3038
- */
3039
- canCombine(_reqA, _reqB) {
3040
- return false;
2810
+ getKeyId(key) {
2811
+ return stableJSONStringify$1(key);
3041
2812
  }
3042
- /**
3043
- * Takes two request configs and combines them into a single request config.
3044
- *
3045
- * @param reqA config of request A
3046
- * @param reqB config of request B
3047
- * @returns
3048
- */
3049
- combineRequests(reqA, _reqB) {
3050
- // By default, this should never be called since requests aren't combinable
3051
- if (process.env.NODE_ENV !== 'production') {
3052
- throw new Error('Not implemented');
3053
- }
3054
- return reqA;
2813
+ saveRequest(key, request) {
2814
+ const identifier = this.getKeyId(key);
2815
+ const batchForKey = this.requestBuffer.get(identifier) || [];
2816
+ batchForKey.push({
2817
+ request,
2818
+ requestTime: Date.now() - this.pageStartTime,
2819
+ });
2820
+ this.requestBuffer.set(identifier, batchForKey);
3055
2821
  }
3056
- /**
3057
- * Checks adapter config against request context to determine if the request is context dependent.
3058
- *
3059
- * By default, requests are not context dependent.
3060
- * @param request
3061
- * @returns
3062
- */
3063
- isContextDependent(_context, _request) {
3064
- return false;
2822
+ getPage(key) {
2823
+ const identifier = stableJSONStringify$1(key);
2824
+ return this.storage.get(identifier);
3065
2825
  }
3066
- /**
3067
- * This tells PDL that requests of this strategy can only be saved in the similar bucket.
3068
- *
3069
- * @returns boolean
3070
- */
3071
- get onlySavedInSimilar() {
3072
- return false;
2826
+ getPageRequests(key) {
2827
+ const page = this.getPage(key);
2828
+ if (page === undefined) {
2829
+ return [];
2830
+ }
2831
+ return page.requests;
3073
2832
  }
3074
2833
  }
3075
2834
 
3076
- class LexRequestStrategy extends RequestStrategy {
3077
- /**
3078
- * Whether or not requests from this strategies can be boxcarred by Aura.
3079
- * If they are, the lex prefetcher will run predictions of this strategy with a limit,
3080
- * to avoid predictions being boxcared.
3081
- *
3082
- * @returns boolean
3083
- */
3084
- get isBoxcarable() {
3085
- return true;
2835
+ const GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME = 'getRelatedListRecordsBatch';
2836
+ class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrategy {
2837
+ constructor() {
2838
+ super(...arguments);
2839
+ this.adapterName = GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME;
2840
+ this.adapterFactory = getRelatedListRecordsBatchAdapterFactory;
3086
2841
  }
3087
- }
3088
-
3089
- const noop = () => { };
3090
- // Taken from https://sourcegraph.soma.salesforce.com/perforce.soma.salesforce.com/app/main/core/-/blob/ui-global-components/components/one/one/oneController.js?L75
3091
- // In theory this should not be here, but for now, we don't have an alternative,
3092
- // given these are started before o11y tells PDL the EPT windows ended (even before the timestamp sent by o11y).
3093
- const onePreloads = new Set([
3094
- 'markup://emailui:formattedEmailWrapper',
3095
- 'markup://emailui:outputEmail',
3096
- 'markup://flexipage:baseRecordHomeTemplateDesktop',
3097
- 'markup://force:actionWindowLink',
3098
- 'markup://force:inlineEditCell',
3099
- 'markup://force:inputField',
3100
- 'markup://force:recordPreviewItem',
3101
- 'markup://force:relatedListDesktop',
3102
- 'markup://force:relatedListQuickLinksContainer',
3103
- 'markup://lst:relatedListQuickLinksContainer',
3104
- 'markup://lst:secondDegreeRelatedListSingleContainer',
3105
- 'markup://lst:bundle_act_coreListViewManagerDesktop',
3106
- 'markup://lst:bundle_act_coreListViewManagerDesktop_generatedTemplates',
3107
- 'markup://lst:baseFilterPanel',
3108
- 'markup://lst:chartPanel',
3109
- 'markup://force:socialPhotoWrapper',
3110
- 'markup://forceContent:contentVersionsEditWizard',
3111
- 'markup://forceContent:outputTitle',
3112
- 'markup://forceContent:virtualRelatedListStencil',
3113
- 'markup://forceSearch:resultsFilters',
3114
- 'markup://interop:unstable_uiRecordApi',
3115
- 'markup://lightning:formattedPhone',
3116
- 'markup://notes:contentNoteRelatedListStencil',
3117
- 'markup://one:alohaPage',
3118
- 'markup://one:consoleObjectHome',
3119
- 'markup://one:recordActionWrapper',
3120
- 'markup://records:lwcDetailPanel',
3121
- 'markup://records:lwcHighlightsPanel',
3122
- 'markup://records:recordLayoutInputDateTime',
3123
- 'markup://records:recordLayoutInputLocation',
3124
- 'markup://records:recordLayoutItem',
3125
- 'markup://records:recordLayoutLookup',
3126
- 'markup://records:recordLayoutRichText',
3127
- 'markup://records:recordLayoutRow',
3128
- 'markup://records:recordLayoutSection',
3129
- 'markup://records:recordLayoutTextArea',
3130
- 'markup://records:recordPicklist',
3131
- 'markup://sfa:outputNameWithHierarchyIcon',
3132
- 'markup://runtime_platform_actions:actionHeadlessFormCancel',
3133
- 'markup://runtime_platform_actions:actionHeadlessFormSave',
3134
- 'markup://runtime_platform_actions:actionHeadlessFormSaveAndNew',
3135
- 'markup://lightning:iconSvgTemplatesCustom',
3136
- 'markup://lightning:iconSvgTemplatesDocType',
3137
- 'markup://record_flexipage:recordHomeFlexipageUtil',
3138
- 'markup://record_flexipage:recordFieldInstancesHandlers',
3139
- 'markup://force:outputCustomLinkUrl',
3140
- 'markup://force:quickActionRunnable',
3141
- 'markup://force:inputURL',
3142
- 'markup://force:inputMultiPicklist',
3143
- 'markup://runtime_sales_activities:activityPanel',
3144
- 'markup://support:outputLookupWithPreviewForSubject',
3145
- 'markup://runtime_sales_activities:activitySubjectListView',
3146
- 'markup://support:outputCaseSubjectField',
3147
- 'markup://sfa:inputOpportunityAmount',
3148
- 'markup://forceChatter:contentFileSize',
3149
- 'markup://flexipage:column2',
3150
- 'markup://sfa:outputNameWithHierarchyIconAccount',
3151
- 'markup://emailui:formattedEmailAccount',
3152
- 'markup://emailui:formattedEmailContact',
3153
- 'markup://emailui:formattedEmailLead',
3154
- 'markup://e.aura:serverActionError',
3155
- 'markup://records:recordType',
3156
- 'markup://flexipage:recordHomeWithSubheaderTemplateDesktop2',
3157
- 'markup://force:customLinkUrl',
3158
- 'markup://sfa:outputOpportunityAmount',
3159
- 'markup://emailui:formattedEmailCase',
3160
- 'markup://runtime_sales_activities:activitySubject',
3161
- 'markup://lightning:quickActionAPI',
3162
- 'markup://force:listViewManagerGridWrapText',
3163
- 'markup://flexipage:recordHomeSimpleViewTemplate2',
3164
- 'markup://flexipage:accordion2',
3165
- 'markup://flexipage:accordionSection2',
3166
- 'markup://flexipage:field',
3167
- 'markup://runtime_iag_core:onboardingManager',
3168
- 'markup://records:entityLabel',
3169
- 'markup://records:highlightsHeaderRightContent',
3170
- 'markup://records:formattedRichText',
3171
- 'markup://force:socialRecordAvatarWrapper',
3172
- 'markup://runtime_pipeline_inspector:pipelineInspectorHome',
3173
- 'markup://sfa:inspectionDesktopObjectHome',
3174
- 'markup://records:outputPhone',
3175
- ]);
3176
- function canPreloadDefinition(def) {
3177
- return (def.startsWith('markup://') &&
3178
- !(
3179
- // some "virtual" components from flexipages are with `__` in the name, eg: design templates.
3180
- // Not filtering out them will not cause errors, but will cause a server request that returns with error.
3181
- (def.includes('__') ||
3182
- // any generated template
3183
- def.includes('forceGenerated') ||
3184
- // part of onePreload
3185
- def.includes('one:onePreloads') ||
3186
- onePreloads.has(def))));
3187
- }
3188
- function requestComponents(config) {
3189
- // Because { foo: undefined } can't be saved in indexedDB (serialization is {})
3190
- // we need to manually save it as { "foo": "" }, and transform it to { foo: undefined }
3191
- const descriptorsMap = {};
3192
- let hasComponentsToLoad = false;
3193
- for (const [def, uid] of entries(config)) {
3194
- if (canPreloadDefinition(def)) {
3195
- hasComponentsToLoad = true;
3196
- descriptorsMap[def] = uid === '' ? undefined : uid;
3197
- }
2842
+ buildConcreteRequest(similarRequest, context) {
2843
+ return {
2844
+ ...similarRequest,
2845
+ config: {
2846
+ ...similarRequest.config,
2847
+ parentRecordId: context.recordId,
2848
+ },
2849
+ };
2850
+ }
2851
+ transformForSaveSimilarRequest(request) {
2852
+ return this.transformForSave({
2853
+ ...request,
2854
+ config: {
2855
+ ...request.config,
2856
+ parentRecordId: '*',
2857
+ },
2858
+ });
2859
+ }
2860
+ isContextDependent(context, request) {
2861
+ return context.recordId === request.config.parentRecordId;
2862
+ }
2863
+ /**
2864
+ * Can combine two seperate batch requests if the parentRecordId is the same.
2865
+ * @param reqA The first GetRelatedListRecordsBatchConfig.
2866
+ * @param reqB The first GetRelatedListRecordsBatchConfig.
2867
+ * @returns true if the requests can be combined, otherwise false.
2868
+ */
2869
+ canCombine(reqA, reqB) {
2870
+ return reqA.parentRecordId === reqB.parentRecordId;
3198
2871
  }
3199
- if (hasComponentsToLoad) {
3200
- unstable_loadComponentDefs(descriptorsMap, noop);
2872
+ /**
2873
+ * Merge the relatedListParameters together between two combinable batch requests.
2874
+ * @param reqA The first GetRelatedListRecordsBatchConfig.
2875
+ * @param reqB The first GetRelatedListRecordsBatchConfig.
2876
+ * @returns The combined request.
2877
+ */
2878
+ combineRequests(reqA, reqB) {
2879
+ const relatedListParametersMap = new Set(reqA.relatedListParameters.map((relatedListParameter) => {
2880
+ return stableJSONStringify$1(relatedListParameter);
2881
+ }));
2882
+ const reqBRelatedListParametersToAdd = reqB.relatedListParameters.filter((relatedListParameter) => {
2883
+ return !relatedListParametersMap.has(stableJSONStringify$1(relatedListParameter));
2884
+ });
2885
+ reqA.relatedListParameters = reqA.relatedListParameters.concat(reqBRelatedListParametersToAdd);
2886
+ return reqA;
3201
2887
  }
3202
2888
  }
3203
- class GetComponentsDefStrategy extends LexRequestStrategy {
2889
+
2890
+ const GET_RELATED_LIST_RECORDS_ADAPTER_NAME = 'getRelatedListRecords';
2891
+ class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
3204
2892
  constructor() {
3205
2893
  super(...arguments);
3206
- this.adapterName = 'getComponentsDef';
3207
- }
3208
- execute(config) {
3209
- return requestComponents(config);
2894
+ this.adapterName = GET_RELATED_LIST_RECORDS_ADAPTER_NAME;
2895
+ this.adapterFactory = getRelatedListRecordsAdapterFactory;
3210
2896
  }
3211
- buildConcreteRequest(similarRequest, _context) {
2897
+ buildConcreteRequest(similarRequest, context) {
3212
2898
  return {
3213
2899
  ...similarRequest,
2900
+ config: {
2901
+ ...similarRequest.config,
2902
+ parentRecordId: context.recordId,
2903
+ },
3214
2904
  };
3215
2905
  }
3216
- transformForSave(request) {
3217
- const normalizedConfig = {};
3218
- for (const [def, uid] of entries(request.config || {})) {
3219
- const normalizedDescriptorName = def.indexOf('://') === -1 ? 'markup://' + def : def;
3220
- // uid can be a string, an object, or undefined.
3221
- // when is an object or undefined, we can't say anything about the version,
3222
- // and we can't save it as `undefined` as it can't be persisted to indexed db.
3223
- normalizedConfig[normalizedDescriptorName] = typeof uid === 'string' ? uid : '';
3224
- }
3225
- return {
3226
- ...request,
3227
- config: normalizedConfig,
3228
- };
3229
- }
3230
- canCombine() {
3231
- return true;
3232
- }
3233
- combineRequests(reqA, reqB) {
3234
- const combinedDescriptors = {};
3235
- // Note the order is important [reqA, reqB], reqB is always after reqA, and we want to keep the last seen uid
3236
- // of a specific component.
3237
- for (const descriptorMap of [reqA, reqB]) {
3238
- for (const [def, uid] of entries(descriptorMap)) {
3239
- if (canPreloadDefinition(def)) {
3240
- combinedDescriptors[def] = uid;
3241
- }
3242
- }
3243
- }
3244
- return combinedDescriptors;
3245
- }
3246
- get onlySavedInSimilar() {
3247
- // Important: tells PDL to save this request only in the similar buckets.
3248
- return true;
3249
- }
3250
- isContextDependent(_context, _request) {
3251
- return true;
3252
- }
3253
2906
  /**
3254
- * Component predictions are not boxcared
3255
2907
  *
3256
- * @returns false
2908
+ * This method returns GetRelatedListRecordsRequest[] that won't be part of a batch request.
2909
+ *
2910
+ * ADG currently handles the batching of GetRelatedListRecords -> GetRelatedListRecordsBatch
2911
+ * https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListRecordsBatchReducer.js
2912
+ *
2913
+ * For performance reasons (fear to overfetch), we only check that the Single relatedListId is not present in any of the Batch requests,
2914
+ * but we don't check for any other parameters.
2915
+ *
2916
+ * @param unfilteredRequests All of the request available for predictions.
2917
+ * @returns GetRelatedListRecordsRequest[] That should be a prediction.
3257
2918
  */
3258
- get isBoxcarable() {
3259
- return false;
2919
+ reduce(unfilteredRequests) {
2920
+ // Batch requests by [parentRecordId]->[RelatedListIds]
2921
+ const batchRequests = unfilteredRequests.filter((entry) => entry.request.adapterName === GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME).reduce((acc, entry) => {
2922
+ // required properties, enforced by adapter typecheck
2923
+ const { parentRecordId, relatedListParameters } = entry.request.config;
2924
+ const existingRlSet = acc.get(parentRecordId) || new Set();
2925
+ // relatedListId enforced by adapter typecheck
2926
+ relatedListParameters.forEach((rlParam) => existingRlSet.add(rlParam.relatedListId));
2927
+ acc.set(parentRecordId, existingRlSet);
2928
+ return acc;
2929
+ }, new Map());
2930
+ const singleRequests = unfilteredRequests.filter((entry) => entry.request.adapterName === this.adapterName);
2931
+ return singleRequests.filter((entry) => {
2932
+ // required props enforced by adapter typecheck
2933
+ const { parentRecordId, relatedListId } = entry.request.config;
2934
+ const batchForParentRecordId = batchRequests.get(parentRecordId);
2935
+ return !(batchForParentRecordId && batchForParentRecordId.has(relatedListId));
2936
+ });
2937
+ }
2938
+ transformForSaveSimilarRequest(request) {
2939
+ return this.transformForSave({
2940
+ ...request,
2941
+ config: {
2942
+ ...request.config,
2943
+ parentRecordId: '*',
2944
+ },
2945
+ });
2946
+ }
2947
+ isContextDependent(context, request) {
2948
+ return context.recordId === request.config.parentRecordId;
3260
2949
  }
3261
2950
  }
3262
2951
 
3263
- const LDS_PDL_CMP_IDENTIFIER = 'lds:pdl';
3264
- const DEFAULT_RESOURCE_CONTEXT = {
2952
+ const APEX_RESOURCE_CONTEXT = {
2953
+ ...DEFAULT_RESOURCE_CONTEXT,
3265
2954
  sourceContext: {
3266
- tagName: LDS_PDL_CMP_IDENTIFIER,
3267
- actionConfig: {
3268
- background: false,
3269
- hotspot: true,
3270
- longRunning: false,
3271
- },
2955
+ ...DEFAULT_RESOURCE_CONTEXT.sourceContext,
2956
+ // We don't want to override anything for Apex, it is not part
2957
+ // of UiApi, and it can cause undesired behavior.
2958
+ actionConfig: undefined,
3272
2959
  },
3273
2960
  };
3274
- class LuvioAdapterRequestStrategy extends LexRequestStrategy {
3275
- constructor(luvio) {
3276
- super();
3277
- this.luvio = luvio;
2961
+ function getApexPdlFactory(luvio) {
2962
+ return ({ invokerParams, config }, requestContext) => {
2963
+ return GetApexWireAdapterFactory(luvio, invokerParams)(config, requestContext);
2964
+ };
2965
+ }
2966
+ const GET_APEX_ADAPTER_NAME = 'getApex';
2967
+ class GetApexRequestStrategy extends LuvioAdapterRequestStrategy {
2968
+ constructor() {
2969
+ super(...arguments);
2970
+ this.adapterName = GET_APEX_ADAPTER_NAME;
2971
+ this.adapterFactory = getApexPdlFactory;
3278
2972
  }
3279
- execute(config, requestContext) {
3280
- return this.adapterFactory(this.luvio)(config, {
3281
- ...DEFAULT_RESOURCE_CONTEXT,
3282
- ...requestContext,
3283
- });
2973
+ buildConcreteRequest(similarRequest) {
2974
+ return similarRequest;
2975
+ }
2976
+ execute(config, _requestContext) {
2977
+ return super.execute(config, APEX_RESOURCE_CONTEXT);
3284
2978
  }
3285
2979
  }
3286
2980
 
3287
- function normalizeRecordIds$1(recordIds) {
3288
- if (!Array.isArray(recordIds)) {
3289
- return [recordIds];
2981
+ const GET_LIST_INFO_BY_NAME_ADAPTER_NAME = 'getListInfoByName';
2982
+ class GetListInfoByNameRequestStrategy extends LuvioAdapterRequestStrategy {
2983
+ constructor() {
2984
+ super(...arguments);
2985
+ this.adapterName = GET_LIST_INFO_BY_NAME_ADAPTER_NAME;
2986
+ this.adapterFactory = getListInfoByNameAdapterFactory;
2987
+ }
2988
+ buildConcreteRequest(similarRequest) {
2989
+ return similarRequest;
2990
+ }
2991
+ transformForSave(request) {
2992
+ return {
2993
+ ...request,
2994
+ config: {
2995
+ ...request.config,
2996
+ // (!): if we are saving this request is because the adapter already verified is valid.
2997
+ objectApiName: coerceObjectId(request.config.objectApiName),
2998
+ },
2999
+ };
3000
+ }
3001
+ canCombine(reqA, reqB) {
3002
+ return (reqA.objectApiName === reqB.objectApiName &&
3003
+ reqA.listViewApiName === reqB.listViewApiName);
3004
+ }
3005
+ combineRequests(reqA, _reqB) {
3006
+ return reqA;
3290
3007
  }
3291
- return recordIds;
3292
3008
  }
3293
- class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
3009
+
3010
+ const GET_LIST_INFOS_BY_OBJECT_NAME_ADAPTER_NAME = 'getListInfosByObjectName';
3011
+ class GetListInfosByObjectNameRequestStrategy extends LuvioAdapterRequestStrategy {
3294
3012
  constructor() {
3295
3013
  super(...arguments);
3296
- this.adapterName = 'getRecordAvatars';
3297
- this.adapterFactory = getRecordAvatarsAdapterFactory;
3014
+ this.adapterName = GET_LIST_INFOS_BY_OBJECT_NAME_ADAPTER_NAME;
3015
+ this.adapterFactory = getListInfosByObjectNameAdapterFactory;
3298
3016
  }
3299
- buildConcreteRequest(similarRequest, context) {
3017
+ buildConcreteRequest(similarRequest) {
3018
+ return similarRequest;
3019
+ }
3020
+ transformForSave(request) {
3300
3021
  return {
3301
- ...similarRequest,
3022
+ ...request,
3302
3023
  config: {
3303
- ...similarRequest.config,
3304
- recordIds: [context.recordId],
3024
+ ...request.config,
3025
+ // (!): if we are saving this request is because the adapter already verified is valid.
3026
+ objectApiName: coerceObjectId(request.config.objectApiName),
3305
3027
  },
3306
3028
  };
3307
3029
  }
3308
- transformForSaveSimilarRequest(request) {
3309
- return this.transformForSave({
3030
+ canCombine(reqA, reqB) {
3031
+ return (reqA.objectApiName === reqB.objectApiName &&
3032
+ reqA.q === reqB.q &&
3033
+ reqA.recentListsOnly === reqB.recentListsOnly &&
3034
+ reqA.pageSize === reqB.pageSize);
3035
+ }
3036
+ combineRequests(reqA, _reqB) {
3037
+ return reqA;
3038
+ }
3039
+ }
3040
+
3041
+ const GET_LIST_RECORDS_BY_NAME_ADAPTER_NAME = 'getListRecordsByName';
3042
+ class GetListRecordsByNameRequestStrategy extends LuvioAdapterRequestStrategy {
3043
+ constructor() {
3044
+ super(...arguments);
3045
+ this.adapterName = GET_LIST_RECORDS_BY_NAME_ADAPTER_NAME;
3046
+ this.adapterFactory = getListRecordsByNameAdapterFactory;
3047
+ }
3048
+ buildConcreteRequest(similarRequest) {
3049
+ return similarRequest;
3050
+ }
3051
+ transformForSave(request) {
3052
+ if (request.config.fields === undefined && request.config.optionalFields === undefined) {
3053
+ return request;
3054
+ }
3055
+ let fields = request.config.fields || [];
3056
+ let optionalFields = request.config.optionalFields || [];
3057
+ return {
3310
3058
  ...request,
3311
3059
  config: {
3312
3060
  ...request.config,
3313
- recordIds: ['*'],
3061
+ fields: [],
3062
+ optionalFields: Array.from(new Set([...fields, ...optionalFields])),
3314
3063
  },
3315
- });
3316
- }
3317
- isContextDependent(context, request) {
3318
- return (request.config.recordIds &&
3319
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
3320
- (request.config.recordIds.length === 1 &&
3321
- request.config.recordIds[0] === context.recordId)));
3064
+ };
3322
3065
  }
3323
3066
  canCombine(reqA, reqB) {
3324
- return reqA.formFactor === reqB.formFactor;
3067
+ return (reqA.objectApiName === reqB.objectApiName &&
3068
+ reqA.listViewApiName === reqB.listViewApiName &&
3069
+ reqA.pageSize === reqB.pageSize &&
3070
+ reqA.searchTerm === reqB.searchTerm &&
3071
+ reqA.sortBy === reqB.sortBy &&
3072
+ reqA.where === reqB.where);
3325
3073
  }
3326
3074
  combineRequests(reqA, reqB) {
3327
- const combined = { ...reqA };
3328
- combined.recordIds = Array.from(new Set([...normalizeRecordIds$1(reqA.recordIds), ...normalizeRecordIds$1(reqB.recordIds)]));
3329
- return combined;
3075
+ return {
3076
+ ...reqA,
3077
+ optionalFields: Array.from(new Set([...(reqA.optionalFields || []), ...(reqB.optionalFields || [])])),
3078
+ };
3330
3079
  }
3331
3080
  }
3332
3081
 
3333
- const COERCE_FIELD_ID_ARRAY_OPTIONS = { onlyQualifiedFieldNames: true };
3334
- class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
3082
+ const GET_LIST_OBJECT_INFO_ADAPTER_NAME = 'getListObjectInfo';
3083
+ class GetListObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
3335
3084
  constructor() {
3336
3085
  super(...arguments);
3337
- this.adapterName = 'getRecord';
3338
- this.adapterFactory = getRecordAdapterFactory;
3086
+ this.adapterName = GET_LIST_OBJECT_INFO_ADAPTER_NAME;
3087
+ this.adapterFactory = getListObjectInfoAdapterFactory;
3339
3088
  }
3340
- buildConcreteRequest(similarRequest, context) {
3341
- return {
3342
- ...similarRequest,
3343
- config: {
3344
- ...similarRequest.config,
3345
- recordId: context.recordId,
3346
- },
3347
- };
3089
+ buildConcreteRequest(similarRequest) {
3090
+ return similarRequest;
3348
3091
  }
3349
3092
  transformForSave(request) {
3350
- if (request.config.fields === undefined && request.config.optionalFields === undefined) {
3351
- return request;
3352
- }
3353
- let fields = coerceFieldIdArray(request.config.fields, COERCE_FIELD_ID_ARRAY_OPTIONS) || [];
3354
- let optionalFields = coerceFieldIdArray(request.config.optionalFields, COERCE_FIELD_ID_ARRAY_OPTIONS) || [];
3355
3093
  return {
3356
3094
  ...request,
3357
3095
  config: {
3358
3096
  ...request.config,
3359
- fields: undefined,
3360
- optionalFields: [...fields, ...optionalFields],
3097
+ // (!): if we are saving this request is because the adapter already verified is valid.
3098
+ objectApiName: coerceObjectId(request.config.objectApiName),
3361
3099
  },
3362
3100
  };
3363
3101
  }
3364
3102
  canCombine(reqA, reqB) {
3365
- // must be same record and
3366
- return (reqA.recordId === reqB.recordId &&
3367
- // both requests are fields requests
3368
- (reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
3369
- (reqB.fields !== undefined || reqB.optionalFields !== undefined));
3103
+ return reqA.objectApiName === reqB.objectApiName;
3370
3104
  }
3371
- combineRequests(reqA, reqB) {
3372
- const fields = new Set();
3373
- const optionalFields = new Set();
3374
- if (reqA.fields !== undefined) {
3375
- reqA.fields.forEach((field) => fields.add(field));
3376
- }
3377
- if (reqB.fields !== undefined) {
3378
- reqB.fields.forEach((field) => fields.add(field));
3105
+ combineRequests(reqA, _reqB) {
3106
+ return reqA;
3107
+ }
3108
+ }
3109
+
3110
+ const RECORD_HOME_SUPPORTED_ADAPTERS = new Set([
3111
+ GET_APEX_ADAPTER_NAME,
3112
+ GET_COMPONENTS_DEF_ADAPTER_NAME,
3113
+ GET_OBJECT_INFO_ADAPTER_NAME,
3114
+ GET_OBJECT_INFO_BATCH_ADAPTER_NAME,
3115
+ GET_RECORD_ACTIONS_ADAPTER_NAME,
3116
+ GET_RECORD_AVATARS_ADAPTER_NAME,
3117
+ GET_RECORD_ADAPTER_NAME,
3118
+ GET_RECORDS_ADAPTER_NAME,
3119
+ GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME,
3120
+ GET_RELATED_LIST_INFO_ADAPTER_NAME,
3121
+ GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME,
3122
+ GET_RELATED_LIST_RECORDS_ADAPTER_NAME,
3123
+ GET_RELATED_LISTS_ACTIONS_ADAPTER_NAME,
3124
+ ]);
3125
+ class RecordHomePage extends LexDefaultPage {
3126
+ constructor(context, requestStrategyManager, options) {
3127
+ super(context);
3128
+ this.requestStrategyManager = requestStrategyManager;
3129
+ this.options = options;
3130
+ const { recordId: _, ...rest } = this.context;
3131
+ this.similarContext = {
3132
+ recordId: '*',
3133
+ ...rest,
3134
+ };
3135
+ }
3136
+ supportsRequest(request) {
3137
+ return RECORD_HOME_SUPPORTED_ADAPTERS.has(request.adapterName);
3138
+ }
3139
+ buildSaveRequestData(request) {
3140
+ const requestBuckets = [];
3141
+ const { adapterName } = request;
3142
+ const matchingRequestStrategy = this.requestStrategyManager.get(adapterName);
3143
+ if (matchingRequestStrategy === undefined) {
3144
+ return [];
3379
3145
  }
3380
- if (reqA.optionalFields !== undefined) {
3381
- reqA.optionalFields.forEach((field) => optionalFields.add(field));
3146
+ if (matchingRequestStrategy.isContextDependent(this.context, request)) {
3147
+ requestBuckets.push({
3148
+ context: this.similarContext,
3149
+ request: matchingRequestStrategy.transformForSaveSimilarRequest(request),
3150
+ });
3151
+ // When `options.useExactMatchesPlus` is not enabled, we can save this request on the similar bucket only
3152
+ if (!this.options.useExactMatchesPlus) {
3153
+ return requestBuckets;
3154
+ }
3382
3155
  }
3383
- if (reqB.optionalFields !== undefined) {
3384
- reqB.optionalFields.forEach((field) => optionalFields.add(field));
3156
+ if (!matchingRequestStrategy.onlySavedInSimilar) {
3157
+ requestBuckets.push({
3158
+ context: this.context,
3159
+ request: matchingRequestStrategy.transformForSave(request),
3160
+ });
3385
3161
  }
3386
- return {
3387
- recordId: reqA.recordId,
3388
- fields: Array.from(fields),
3389
- optionalFields: Array.from(optionalFields),
3390
- };
3162
+ return requestBuckets;
3391
3163
  }
3392
- isContextDependent(context, request) {
3393
- return request.config.recordId === context.recordId;
3164
+ resolveSimilarRequest(similarRequest) {
3165
+ const { adapterName } = similarRequest;
3166
+ const matchingRequestStrategy = this.requestStrategyManager.get(adapterName);
3167
+ if (matchingRequestStrategy === undefined) {
3168
+ return similarRequest;
3169
+ }
3170
+ return matchingRequestStrategy.buildConcreteRequest(similarRequest, this.context);
3394
3171
  }
3395
- transformForSaveSimilarRequest(request) {
3396
- return this.transformForSave({
3397
- ...request,
3398
- config: {
3399
- ...request.config,
3400
- recordId: '*',
3172
+ // Record Home performs best when we always do a minimal getRecord alongside the other requests.
3173
+ getAlwaysRunRequests() {
3174
+ const { recordId, objectApiName } = this.context;
3175
+ return [
3176
+ {
3177
+ adapterName: 'getRecord',
3178
+ config: {
3179
+ recordId,
3180
+ optionalFields: [`${objectApiName}.Id`, `${objectApiName}.RecordTypeId`],
3181
+ },
3401
3182
  },
3402
- });
3183
+ ];
3184
+ }
3185
+ /**
3186
+ * In RH, we know that there will be predictions, and we want to reduce the always requests (getRecord(id, type))
3187
+ * with one of the predictions in case some request containing the fields was missed in the predictions.
3188
+ *
3189
+ * @returns true
3190
+ */
3191
+ shouldReduceAlwaysRequestsWithPredictions() {
3192
+ return true;
3193
+ }
3194
+ /**
3195
+ * In RH, we should execute the getRecord(id, type) by itself as we want the result asap, so
3196
+ * it does not stop rendering.
3197
+ * @returns true
3198
+ */
3199
+ shouldExecuteAlwaysRequestByThemself() {
3200
+ return true;
3201
+ }
3202
+ static handlesContext(context) {
3203
+ return (context !== undefined &&
3204
+ context.actionName !== undefined &&
3205
+ context.objectApiName !== undefined &&
3206
+ context.recordId !== undefined &&
3207
+ context.type === 'recordPage');
3403
3208
  }
3404
3209
  }
3405
3210
 
3406
- class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
3407
- constructor() {
3408
- super(...arguments);
3409
- this.adapterName = 'getRecords';
3410
- this.adapterFactory = getRecordsAdapterFactory;
3211
+ const OBJECT_HOME_SUPPORTED_ADAPTERS = new Set([
3212
+ GET_LIST_INFO_BY_NAME_ADAPTER_NAME,
3213
+ GET_LIST_OBJECT_INFO_ADAPTER_NAME,
3214
+ GET_LIST_RECORDS_BY_NAME_ADAPTER_NAME,
3215
+ GET_LIST_INFOS_BY_OBJECT_NAME_ADAPTER_NAME,
3216
+ GET_OBJECT_INFO_BATCH_ADAPTER_NAME,
3217
+ ]);
3218
+ class ObjectHomePage extends LexDefaultPage {
3219
+ constructor(context, requestStrategyManager, options) {
3220
+ super(context);
3221
+ this.requestStrategyManager = requestStrategyManager;
3222
+ this.options = options;
3223
+ this.similarContext = context;
3411
3224
  }
3412
- buildConcreteRequest(similarRequest, context) {
3413
- return {
3414
- ...similarRequest,
3415
- config: {
3416
- ...similarRequest.config,
3417
- records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
3418
- },
3419
- };
3225
+ supportsRequest(request) {
3226
+ return OBJECT_HOME_SUPPORTED_ADAPTERS.has(request.adapterName);
3420
3227
  }
3421
- isContextDependent(context, request) {
3422
- const isSingleRecordRequest = request.config.records.length === 1 && request.config.records[0].recordIds.length === 1;
3423
- return isSingleRecordRequest && request.config.records[0].recordIds[0] === context.recordId;
3228
+ buildSaveRequestData(request) {
3229
+ const requestBuckets = [];
3230
+ const { adapterName } = request;
3231
+ const matchingRequestStrategy = this.requestStrategyManager.get(adapterName);
3232
+ if (matchingRequestStrategy === undefined) {
3233
+ return [];
3234
+ }
3235
+ if (matchingRequestStrategy.isContextDependent(this.context, request)) {
3236
+ requestBuckets.push({
3237
+ context: this.similarContext,
3238
+ request: matchingRequestStrategy.transformForSaveSimilarRequest(request),
3239
+ });
3240
+ // When `options.useExactMatchesPlus` is not enabled, we can save this request on the similar bucket only
3241
+ if (!this.options.useExactMatchesPlus) {
3242
+ return requestBuckets;
3243
+ }
3244
+ }
3245
+ requestBuckets.push({
3246
+ context: this.context,
3247
+ request: matchingRequestStrategy.transformForSave(request),
3248
+ });
3249
+ return requestBuckets;
3424
3250
  }
3425
- transformForSaveSimilarRequest(request) {
3426
- return this.transformForSave({
3427
- ...request,
3428
- config: {
3429
- ...request.config,
3430
- records: [
3431
- {
3432
- ...request.config.records[0],
3433
- recordIds: ['*'],
3434
- },
3435
- ],
3251
+ // no similar requests between LVs
3252
+ resolveSimilarRequest(similarRequest) {
3253
+ return similarRequest;
3254
+ }
3255
+ // these are requests that run always regardless of any other request existing
3256
+ getAlwaysRunRequests() {
3257
+ const { listViewApiName, objectApiName } = this.context;
3258
+ return [
3259
+ {
3260
+ adapterName: 'getListInfoByName',
3261
+ config: {
3262
+ objectApiName: objectApiName,
3263
+ listViewApiName: listViewApiName,
3264
+ },
3436
3265
  },
3437
- });
3266
+ {
3267
+ adapterName: 'getListInfosByObjectName',
3268
+ config: {
3269
+ objectApiName: objectApiName,
3270
+ pageSize: 100,
3271
+ q: '',
3272
+ },
3273
+ },
3274
+ {
3275
+ adapterName: 'getListInfosByObjectName',
3276
+ config: {
3277
+ objectApiName: objectApiName,
3278
+ pageSize: 10,
3279
+ recentListsOnly: true,
3280
+ },
3281
+ },
3282
+ {
3283
+ adapterName: 'getListObjectInfo',
3284
+ config: {
3285
+ objectApiName: objectApiName,
3286
+ },
3287
+ },
3288
+ ];
3438
3289
  }
3439
- }
3440
-
3441
- function normalizeRecordIds(recordIds) {
3442
- if (!isArray(recordIds)) {
3443
- return [recordIds];
3290
+ /**
3291
+ * AlwaysRequests must be reduced with predictions.
3292
+ *
3293
+ * @returns true
3294
+ */
3295
+ shouldReduceAlwaysRequestsWithPredictions() {
3296
+ return true;
3444
3297
  }
3445
- return recordIds;
3446
- }
3447
- function normalizeApiNames(apiNames) {
3448
- if (apiNames === undefined || apiNames === null) {
3449
- return [];
3298
+ /**
3299
+ * In OH, the always requests are reduced with predictions, and because they
3300
+ * can't be merged with other predictions, they will always run by themself.
3301
+ * This value must be `false`, otherwise we may see repeated requests.
3302
+ *
3303
+ * @returns false
3304
+ */
3305
+ shouldExecuteAlwaysRequestByThemself() {
3306
+ return false;
3307
+ }
3308
+ // Identifies a valid ObjectHomeContext
3309
+ static handlesContext(context) {
3310
+ return (context !== undefined &&
3311
+ context.listViewApiName !== undefined &&
3312
+ context.objectApiName !== undefined &&
3313
+ context.type === 'objectHomePage');
3450
3314
  }
3451
- return isArray(apiNames) ? apiNames : [apiNames];
3452
3315
  }
3453
- class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
3454
- constructor() {
3455
- super(...arguments);
3456
- this.adapterName = 'getRecordActions';
3457
- this.adapterFactory = getRecordActionsAdapterFactory;
3458
- }
3459
- buildConcreteRequest(similarRequest, context) {
3316
+
3317
+ /**
3318
+ * Observability / Critical Availability Program (230+)
3319
+ *
3320
+ * This file is intended to be used as a consolidated place for all definitions, functions,
3321
+ * and helpers related to "M1"[1].
3322
+ *
3323
+ * Below are the R.E.A.D.S. metrics for the Lightning Data Service, defined here[2].
3324
+ *
3325
+ * [1] Search "[M1] Lightning Data Service Design Spike" in Quip
3326
+ * [2] Search "Lightning Data Service R.E.A.D.S. Metrics" in Quip
3327
+ */
3328
+ const OBSERVABILITY_NAMESPACE = 'LIGHTNING.lds.service';
3329
+ const ADAPTER_INVOCATION_COUNT_METRIC_NAME = 'request';
3330
+ const ADAPTER_ERROR_COUNT_METRIC_NAME = 'error';
3331
+ const NETWORK_ADAPTER_RESPONSE_METRIC_NAME = 'network-response';
3332
+ /**
3333
+ * W-8379680
3334
+ * Counter for number of getApex requests.
3335
+ */
3336
+ const GET_APEX_REQUEST_COUNT = {
3337
+ get() {
3460
3338
  return {
3461
- ...similarRequest,
3462
- config: {
3463
- ...similarRequest.config,
3464
- recordIds: [context.recordId],
3465
- },
3339
+ owner: OBSERVABILITY_NAMESPACE,
3340
+ name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
3466
3341
  };
3342
+ },
3343
+ };
3344
+ /**
3345
+ * W-8828410
3346
+ * Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
3347
+ */
3348
+ const TOTAL_ADAPTER_ERROR_COUNT = {
3349
+ get() {
3350
+ return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
3351
+ },
3352
+ };
3353
+ /**
3354
+ * W-8828410
3355
+ * Counter for the number of invocations made into LDS by a wire adapter.
3356
+ */
3357
+ const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
3358
+ get() {
3359
+ return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
3360
+ },
3361
+ };
3362
+
3363
+ /**
3364
+ * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
3365
+ * This is needed because insertion order for JSON.stringify(object) affects output:
3366
+ * JSON.stringify({a: 1, b: 2})
3367
+ * "{"a":1,"b":2}"
3368
+ * JSON.stringify({b: 2, a: 1})
3369
+ * "{"b":2,"a":1}"
3370
+ * Modified from the apex implementation to sort arrays non-destructively.
3371
+ * @param data Data to be JSON-stringified.
3372
+ * @returns JSON.stringified value with consistent ordering of keys.
3373
+ */
3374
+ function stableJSONStringify(node) {
3375
+ // This is for Date values.
3376
+ if (node && node.toJSON && typeof node.toJSON === 'function') {
3377
+ // eslint-disable-next-line no-param-reassign
3378
+ node = node.toJSON();
3467
3379
  }
3468
- transformForSaveSimilarRequest(request) {
3469
- return this.transformForSave({
3470
- ...request,
3471
- config: {
3472
- ...request.config,
3473
- recordIds: ['*'],
3474
- },
3475
- });
3380
+ if (node === undefined) {
3381
+ return;
3476
3382
  }
3477
- canCombine(reqA, reqB) {
3478
- return (reqA.retrievalMode === reqB.retrievalMode &&
3479
- reqA.formFactor === reqB.formFactor &&
3480
- (reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
3481
- (reqA.sections || []).toString() === (reqB.sections || []).toString());
3383
+ if (typeof node === 'number') {
3384
+ return isFinite(node) ? '' + node : 'null';
3482
3385
  }
3483
- combineRequests(reqA, reqB) {
3484
- const combined = { ...reqA };
3485
- // let's merge the recordIds
3486
- combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
3487
- if (combined.retrievalMode === 'ALL') {
3488
- const combinedSet = new Set([
3489
- ...normalizeApiNames(combined.apiNames),
3490
- ...normalizeApiNames(reqB.apiNames),
3491
- ]);
3492
- combined.apiNames = Array.from(combinedSet);
3386
+ if (typeof node !== 'object') {
3387
+ return stringify(node);
3388
+ }
3389
+ let i;
3390
+ let out;
3391
+ if (isArray(node)) {
3392
+ // copy any array before sorting so we don't mutate the object.
3393
+ // eslint-disable-next-line no-param-reassign
3394
+ node = node.slice(0).sort();
3395
+ out = '[';
3396
+ for (i = 0; i < node.length; i++) {
3397
+ if (i) {
3398
+ out += ',';
3399
+ }
3400
+ out += stableJSONStringify(node[i]) || 'null';
3493
3401
  }
3494
- return combined;
3402
+ return out + ']';
3495
3403
  }
3496
- isContextDependent(context, request) {
3497
- return (request.config.recordIds &&
3498
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
3499
- (request.config.recordIds.length === 1 &&
3500
- request.config.recordIds[0] === context.recordId)));
3404
+ if (node === null) {
3405
+ return 'null';
3406
+ }
3407
+ const keys$1 = keys(node).sort();
3408
+ out = '';
3409
+ for (i = 0; i < keys$1.length; i++) {
3410
+ const key = keys$1[i];
3411
+ const value = stableJSONStringify(node[key]);
3412
+ if (!value) {
3413
+ continue;
3414
+ }
3415
+ if (out) {
3416
+ out += ',';
3417
+ }
3418
+ out += stringify(key) + ':' + value;
3501
3419
  }
3420
+ return '{' + out + '}';
3502
3421
  }
3503
-
3504
- const GET_OBJECT_INFO_BATCH_ADAPTER_NAME = 'getObjectInfos';
3505
- /**
3506
- * Returns true if A is a superset of B
3507
- * @param a
3508
- * @param b
3509
- */
3510
- function isSuperSet(a, b) {
3511
- return b.every((oan) => a.has(oan));
3422
+ function isPromise(value) {
3423
+ // check for Thenable due to test frameworks using custom Promise impls
3424
+ return value !== null && value.then !== undefined;
3512
3425
  }
3513
- class GetObjectInfosRequestStrategy extends LuvioAdapterRequestStrategy {
3426
+
3427
+ const APEX_ADAPTER_NAME = 'getApex';
3428
+ const NORMALIZED_APEX_ADAPTER_NAME = `Apex.${APEX_ADAPTER_NAME}`;
3429
+ const REFRESH_APEX_KEY = 'refreshApex';
3430
+ const REFRESH_UIAPI_KEY = 'refreshUiApi';
3431
+ const SUPPORTED_KEY = 'refreshSupported';
3432
+ const UNSUPPORTED_KEY = 'refreshUnsupported';
3433
+ const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
3434
+ const REFRESH_EVENTTYPE = 'system';
3435
+ const REFRESH_PAYLOAD_TARGET = 'adapters';
3436
+ const REFRESH_PAYLOAD_SCOPE = 'lds';
3437
+ const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
3438
+ const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
3439
+ const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
3440
+ const NAMESPACE = 'lds';
3441
+ const NETWORK_TRANSACTION_NAME = 'lds-network';
3442
+ const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
3443
+ // Aggregate Cache Stats and Metrics for all getApex invocations
3444
+ const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
3445
+ const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
3446
+ // Observability (READS)
3447
+ const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
3448
+ const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
3449
+ const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
3450
+ class Instrumentation {
3514
3451
  constructor() {
3515
- super(...arguments);
3516
- this.adapterName = GET_OBJECT_INFO_BATCH_ADAPTER_NAME;
3517
- this.adapterFactory = getObjectInfosAdapterFactory;
3518
- }
3519
- buildConcreteRequest(similarRequest) {
3520
- return similarRequest;
3452
+ this.adapterUnfulfilledErrorCounters = {};
3453
+ this.recordApiNameChangeCounters = {};
3454
+ this.refreshAdapterEvents = {};
3455
+ this.refreshApiCallEventStats = {
3456
+ [REFRESH_APEX_KEY]: 0,
3457
+ [REFRESH_UIAPI_KEY]: 0,
3458
+ [SUPPORTED_KEY]: 0,
3459
+ [UNSUPPORTED_KEY]: 0,
3460
+ };
3461
+ this.lastRefreshApiCall = null;
3462
+ this.weakEtagZeroEvents = {};
3463
+ this.adapterCacheMisses = new LRUCache(250);
3464
+ if (typeof window !== 'undefined' && window.addEventListener) {
3465
+ window.addEventListener('beforeunload', () => {
3466
+ if (keys(this.weakEtagZeroEvents).length > 0) {
3467
+ perfStart(NETWORK_TRANSACTION_NAME);
3468
+ perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
3469
+ }
3470
+ });
3471
+ }
3472
+ registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
3521
3473
  }
3522
- transformForSave(request) {
3523
- return {
3524
- ...request,
3525
- config: {
3526
- ...request.config,
3527
- // (!): if we are saving this request is because the adapter already verified is valid.
3528
- objectApiNames: coerceObjectIdArray(request.config.objectApiNames),
3529
- },
3474
+ /**
3475
+ * Instruments an existing adapter to log argus metrics and cache stats.
3476
+ * @param adapter The adapter function.
3477
+ * @param metadata The adapter metadata.
3478
+ * @param wireConfigKeyFn Optional function to transform wire configs to a unique key.
3479
+ * @returns The wrapped adapter.
3480
+ */
3481
+ instrumentAdapter(adapter, metadata) {
3482
+ // We are consolidating all apex adapter instrumentation calls under a single key
3483
+ const { apiFamily, name, ttl } = metadata;
3484
+ const adapterName = normalizeAdapterName(name, apiFamily);
3485
+ const isGetApexAdapter = isApexAdapter(name);
3486
+ const stats = isGetApexAdapter ? getApexCacheStats : registerLdsCacheStats(adapterName);
3487
+ const ttlMissStats = isGetApexAdapter
3488
+ ? getApexTtlCacheStats
3489
+ : registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
3490
+ /**
3491
+ * W-8076905
3492
+ * Dynamically generated metric. Simple counter for all requests made by this adapter.
3493
+ */
3494
+ const wireAdapterRequestMetric = isGetApexAdapter
3495
+ ? getApexRequestCountMetric
3496
+ : counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
3497
+ const instrumentedAdapter = (config, requestContext) => {
3498
+ // increment overall and adapter request metrics
3499
+ wireAdapterRequestMetric.increment(1);
3500
+ totalAdapterRequestSuccessMetric.increment(1);
3501
+ // execute adapter logic
3502
+ const result = adapter(config, requestContext);
3503
+ // In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
3504
+ // (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
3505
+ //
3506
+ // Note: we can't do a plain instanceof check for a promise here since the Promise may
3507
+ // originate from another javascript realm (for example: in jest test). Instead we use a
3508
+ // duck-typing approach by checking if the result has a then property.
3509
+ //
3510
+ // For adapters without persistent store:
3511
+ // - total cache hit ratio:
3512
+ // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
3513
+ // For adapters with persistent store:
3514
+ // - in-memory cache hit ratio:
3515
+ // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
3516
+ // - total cache hit ratio:
3517
+ // ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
3518
+ // if result === null then config is insufficient/invalid so do not log
3519
+ if (isPromise(result)) {
3520
+ stats.logMisses();
3521
+ if (ttl !== undefined) {
3522
+ this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
3523
+ }
3524
+ }
3525
+ else if (result !== null) {
3526
+ stats.logHits();
3527
+ }
3528
+ return result;
3530
3529
  };
3530
+ // Set the name property on the function for debugging purposes.
3531
+ Object.defineProperty(instrumentedAdapter, 'name', {
3532
+ value: name + '__instrumented',
3533
+ });
3534
+ return instrumentAdapter(instrumentedAdapter, metadata);
3531
3535
  }
3532
3536
  /**
3533
- * Reduces the given GetObjectInfosRequest requests by eliminating those for which config.objectApiNames
3534
- * is a subset of another GetObjectInfosRequest.
3535
- *
3536
- * @param unfilteredRequests - Array of unfiltered requests
3537
- * @returns RequestEntry<GetObjectInfosRequest>[] - Array of reduced requests
3537
+ * Logs when adapter requests come in. If we have subsequent cache misses on a given config, beyond its TTL then log the duration to metrics.
3538
+ * Backed by an LRU Cache implementation to prevent too many record entries from being stored in-memory.
3539
+ * @param name The wire adapter name.
3540
+ * @param config The config passed into wire adapter.
3541
+ * @param ttlMissStats CacheStatsLogger to log misses out of TTL.
3542
+ * @param currentCacheMissTimestamp Timestamp for when the request was made.
3543
+ * @param ttl TTL for the wire adapter.
3538
3544
  */
3539
- reduce(unfilteredRequests) {
3540
- // Filter and sort requests by the length of objectApiNames in ascending order.
3541
- // This ensures a superset of request (i) can only be found in a request (j) such that i < j.
3542
- const objectInfosRequests = this.filterRequests(unfilteredRequests).sort((a, b) => a.request.config.objectApiNames.length - b.request.config.objectApiNames.length);
3543
- // Convert request configurations to sets for easier comparison, avoiding a new set construction each iteration.
3544
- const requestConfigAsSet = objectInfosRequests.map((r) => new Set(r.request.config.objectApiNames));
3545
- const reducedRequests = [];
3546
- // Iterate over each request to determine if it is a subset of others
3547
- for (let i = 0, n = objectInfosRequests.length; i < n; i++) {
3548
- const current = objectInfosRequests[i];
3549
- const { request: { config: currentRequestConfig }, requestMetadata: currentRequestMetadata, } = current;
3550
- let isCurrentSubsetOfOthers = false;
3551
- // Check if the current request is a subset of any subsequent requests
3552
- for (let j = i + 1; j < n; j++) {
3553
- const possibleSuperset = objectInfosRequests[j];
3554
- if (isSuperSet(requestConfigAsSet[j], currentRequestConfig.objectApiNames)) {
3555
- isCurrentSubsetOfOthers = true;
3556
- if (currentRequestMetadata.requestTime <
3557
- possibleSuperset.requestMetadata.requestTime) {
3558
- possibleSuperset.requestMetadata.requestTime =
3559
- currentRequestMetadata.requestTime;
3560
- }
3561
- }
3562
- }
3563
- if (!isCurrentSubsetOfOthers) {
3564
- reducedRequests.push(current);
3545
+ logAdapterCacheMissOutOfTtlDuration(name, config, ttlMissStats, currentCacheMissTimestamp, ttl) {
3546
+ const configKey = `${name}:${stableJSONStringify(config)}`;
3547
+ const existingCacheMissTimestamp = this.adapterCacheMisses.get(configKey);
3548
+ this.adapterCacheMisses.set(configKey, currentCacheMissTimestamp);
3549
+ if (existingCacheMissTimestamp !== undefined) {
3550
+ const duration = currentCacheMissTimestamp - existingCacheMissTimestamp;
3551
+ if (duration > ttl) {
3552
+ ttlMissStats.logMisses();
3565
3553
  }
3566
3554
  }
3567
- return reducedRequests;
3568
- }
3569
- }
3570
-
3571
- class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
3572
- constructor() {
3573
- super(...arguments);
3574
- this.adapterName = 'getObjectInfo';
3575
- this.adapterFactory = getObjectInfoAdapterFactory;
3576
- }
3577
- buildConcreteRequest(similarRequest, context) {
3578
- return {
3579
- ...similarRequest,
3580
- config: {
3581
- ...similarRequest.config,
3582
- objectApiName: context.objectApiName,
3583
- },
3584
- };
3585
- }
3586
- transformForSave(request) {
3587
- return {
3588
- ...request,
3589
- config: {
3590
- ...request.config,
3591
- // (!): if we are saving this request is because the adapter already verified is valid.
3592
- objectApiName: coerceObjectId(request.config.objectApiName),
3593
- },
3594
- };
3595
- }
3596
- isContextDependent(context, request) {
3597
- return (request.config.objectApiName !== undefined &&
3598
- context.objectApiName === request.config.objectApiName);
3599
3555
  }
3600
3556
  /**
3601
- * This method returns GetObjectInfoRequest[] that won't be part of a batch (getObjectInfos) request.
3557
+ * Injected to LDS for Luvio specific instrumentation.
3602
3558
  *
3603
- * @param unfilteredRequests all prediction requests
3604
- * @returns
3559
+ * @param context The transaction context.
3605
3560
  */
3606
- reduce(unfilteredRequests) {
3607
- const objectApiNamesInBatchRequest = unfilteredRequests.filter((entry) => entry.request.adapterName === GET_OBJECT_INFO_BATCH_ADAPTER_NAME).reduce((acc, { request }) => {
3608
- request.config.objectApiNames.forEach((apiName) => acc.add(apiName));
3609
- return acc;
3610
- }, new Set());
3611
- const singleRequests = this.filterRequests(unfilteredRequests);
3612
- return singleRequests.filter((singleEntry) => {
3613
- return !objectApiNamesInBatchRequest.has(singleEntry.request.config.objectApiName);
3614
- });
3615
- }
3616
- }
3617
-
3618
- function isReduceAbleRelatedListConfig(config) {
3619
- return config.relatedListsActionParameters.every((rlReq) => {
3620
- return rlReq.relatedListId !== undefined && keys(rlReq).length === 1;
3621
- });
3622
- }
3623
- class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy {
3624
- constructor() {
3625
- super(...arguments);
3626
- this.adapterName = 'getRelatedListsActions';
3627
- this.adapterFactory = getRelatedListsActionsAdapterFactory;
3628
- }
3629
- buildConcreteRequest(similarRequest, context) {
3630
- return {
3631
- ...similarRequest,
3632
- config: {
3633
- ...similarRequest.config,
3634
- recordIds: [context.recordId],
3635
- },
3636
- };
3561
+ instrumentLuvio(context) {
3562
+ instrumentLuvio(context);
3563
+ if (this.isRefreshAdapterEvent(context)) {
3564
+ this.aggregateRefreshAdapterEvents(context);
3565
+ }
3566
+ else if (this.isAdapterUnfulfilledError(context)) {
3567
+ this.incrementAdapterRequestErrorCount(context);
3568
+ }
3569
+ else ;
3637
3570
  }
3638
- transformForSaveSimilarRequest(request) {
3639
- return this.transformForSave({
3640
- ...request,
3641
- config: {
3642
- ...request.config,
3643
- recordIds: ['*'],
3644
- },
3645
- });
3571
+ /**
3572
+ * Returns whether or not this is a RefreshAdapterEvent.
3573
+ * @param context The transaction context.
3574
+ * @returns Whether or not this is a RefreshAdapterEvent.
3575
+ */
3576
+ isRefreshAdapterEvent(context) {
3577
+ return context[REFRESH_ADAPTER_EVENT] === true;
3646
3578
  }
3647
- isContextDependent(context, request) {
3648
- const isForContext = request.config.recordIds &&
3649
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
3650
- (request.config.recordIds.length === 1 &&
3651
- request.config.recordIds[0] === context.recordId));
3652
- return isForContext && isReduceAbleRelatedListConfig(request.config);
3579
+ /**
3580
+ * Returns whether or not this is an AdapterUnfulfilledError.
3581
+ * @param context The transaction context.
3582
+ * @returns Whether or not this is an AdapterUnfulfilledError.
3583
+ */
3584
+ isAdapterUnfulfilledError(context) {
3585
+ return context[ADAPTER_UNFULFILLED_ERROR] === true;
3653
3586
  }
3654
3587
  /**
3655
- * Can only reduce two requests when they have the same recordId, and
3656
- * the individual relatedListAction config only have relatedListId.
3588
+ * Specific instrumentation for getRecordNotifyChange.
3589
+ * temporary implementation to match existing aura call for now
3657
3590
  *
3658
- * @param reqA
3659
- * @param reqB
3660
- * @returns boolean
3591
+ * @param uniqueWeakEtags whether weakEtags match or not
3592
+ * @param error if dispatchResourceRequest fails for any reason
3661
3593
  */
3662
- canCombine(reqA, reqB) {
3663
- const [recordIdA, recordIdB] = [reqA.recordIds, reqB.recordIds].map((recordIds) => {
3664
- return isArray(recordIds)
3665
- ? recordIds.length === 1
3666
- ? recordIds[0]
3667
- : null
3668
- : recordIds;
3669
- });
3670
- return (recordIdA === recordIdB &&
3671
- recordIdA !== null &&
3672
- isReduceAbleRelatedListConfig(reqA) &&
3673
- isReduceAbleRelatedListConfig(reqB));
3674
- }
3675
- combineRequests(reqA, reqB) {
3676
- const relatedListsIncluded = new Set();
3677
- [reqA, reqB].forEach(({ relatedListsActionParameters }) => {
3678
- relatedListsActionParameters.forEach(({ relatedListId }) => relatedListsIncluded.add(relatedListId));
3679
- });
3680
- return {
3681
- recordIds: reqA.recordIds,
3682
- relatedListsActionParameters: from(relatedListsIncluded).map((relatedListId) => ({
3683
- relatedListId,
3684
- })),
3685
- };
3686
- }
3687
- }
3688
-
3689
- const GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME = 'getRelatedListInfoBatch';
3690
- class GetRelatedListInfoBatchRequestStrategy extends LuvioAdapterRequestStrategy {
3691
- constructor() {
3692
- super(...arguments);
3693
- this.adapterName = GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME;
3694
- this.adapterFactory = getRelatedListInfoBatchAdapterFactory;
3695
- }
3696
- buildConcreteRequest(similarRequest, _context) {
3697
- return similarRequest;
3594
+ notifyChangeNetwork(uniqueWeakEtags, error) {
3595
+ perfStart(NETWORK_TRANSACTION_NAME);
3596
+ if (error === true) {
3597
+ perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
3598
+ }
3599
+ else {
3600
+ perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
3601
+ }
3698
3602
  }
3699
3603
  /**
3700
- * @override
3604
+ * Parses and aggregates weakETagZero events to be sent in summarized log line.
3605
+ * @param context The transaction context.
3701
3606
  */
3702
- transformForSave(request) {
3703
- return {
3704
- ...request,
3705
- config: {
3706
- ...request.config,
3707
- parentObjectApiName: coerceObjectId(request.config.parentObjectApiName),
3708
- },
3709
- };
3710
- }
3711
- canCombine(reqA, reqB) {
3712
- return reqA.parentObjectApiName === reqB.parentObjectApiName;
3713
- }
3714
- combineRequests(reqA, reqB) {
3715
- const combined = { ...reqA };
3716
- combined.relatedListNames = Array.from(new Set([...reqA.relatedListNames, ...reqB.relatedListNames]));
3717
- return combined;
3718
- }
3719
- }
3720
-
3721
- class GetRelatedListInfoRequestStrategy extends LuvioAdapterRequestStrategy {
3722
- constructor() {
3723
- super(...arguments);
3724
- this.adapterName = 'getRelatedListInfo';
3725
- this.adapterFactory = getRelatedListInfoAdapterFactory;
3607
+ aggregateWeakETagEvents(incomingWeakEtagZero, existingWeakEtagZero, apiName) {
3608
+ const key = 'weaketag-0-' + apiName;
3609
+ if (this.weakEtagZeroEvents[key] === undefined) {
3610
+ this.weakEtagZeroEvents[key] = {
3611
+ [EXISTING_WEAKETAG_0_KEY]: 0,
3612
+ [INCOMING_WEAKETAG_0_KEY]: 0,
3613
+ };
3614
+ }
3615
+ if (existingWeakEtagZero) {
3616
+ this.weakEtagZeroEvents[key][EXISTING_WEAKETAG_0_KEY] += 1;
3617
+ }
3618
+ if (incomingWeakEtagZero) {
3619
+ this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
3620
+ }
3726
3621
  }
3727
3622
  /**
3728
- * @override
3623
+ * Aggregates refresh adapter events to be sent in summarized log line.
3624
+ * - how many times refreshApex is called
3625
+ * - how many times refresh from lightning/uiRecordApi is called
3626
+ * - number of supported calls: refreshApex called on apex adapter
3627
+ * - number of unsupported calls: refreshApex on non-apex adapter
3628
+ * + any use of refresh from uiRecordApi module
3629
+ * - count of refresh calls per adapter
3630
+ * @param context The refresh adapter event.
3729
3631
  */
3730
- isContextDependent(context, request) {
3731
- // we have a higher degree of confidence of being able to use a similar request when
3732
- // optional config is not set AND the object type of the page context is the same as the
3733
- // object type related list is for
3734
- return (context.objectApiName === request.config.parentObjectApiName &&
3735
- this.isRequiredOnlyConfig(request.config));
3632
+ aggregateRefreshAdapterEvents(context) {
3633
+ // We are consolidating all apex adapter instrumentation calls under a single key
3634
+ // Adding additional logging that getApex adapters can invoke? Read normalizeAdapterName ts-doc.
3635
+ const adapterName = normalizeAdapterName(context.adapterName);
3636
+ if (this.lastRefreshApiCall === REFRESH_APEX_KEY) {
3637
+ if (isApexAdapter(adapterName)) {
3638
+ this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
3639
+ }
3640
+ else {
3641
+ this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
3642
+ }
3643
+ }
3644
+ else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
3645
+ this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
3646
+ }
3647
+ if (this.refreshAdapterEvents[adapterName] === undefined) {
3648
+ this.refreshAdapterEvents[adapterName] = 0;
3649
+ }
3650
+ this.refreshAdapterEvents[adapterName] += 1;
3651
+ this.lastRefreshApiCall = null;
3736
3652
  }
3737
3653
  /**
3738
- * @override
3654
+ * Increments call stat for incoming refresh api call, and sets the name
3655
+ * to be used in {@link aggregateRefreshCalls}
3656
+ * @param from The name of the refresh function called.
3739
3657
  */
3740
- buildConcreteRequest(similarRequest, _context) {
3741
- return similarRequest;
3658
+ handleRefreshApiCall(apiName) {
3659
+ this.refreshApiCallEventStats[apiName] += 1;
3660
+ // set function call to be used with aggregateRefreshCalls
3661
+ this.lastRefreshApiCall = apiName;
3742
3662
  }
3743
3663
  /**
3744
- * @override
3664
+ * W-7302241
3665
+ * Logs refresh call summary stats as a LightningInteraction.
3745
3666
  */
3746
- transformForSave(request) {
3747
- return {
3748
- ...request,
3749
- config: {
3750
- ...request.config,
3751
- parentObjectApiName: coerceObjectId(request.config.parentObjectApiName),
3752
- },
3667
+ logRefreshStats() {
3668
+ if (keys(this.refreshAdapterEvents).length > 0) {
3669
+ interaction(REFRESH_PAYLOAD_TARGET, REFRESH_PAYLOAD_SCOPE, this.refreshAdapterEvents, REFRESH_EVENTSOURCE, REFRESH_EVENTTYPE, this.refreshApiCallEventStats);
3670
+ this.resetRefreshStats();
3671
+ }
3672
+ }
3673
+ /**
3674
+ * Resets the stat trackers for refresh call events.
3675
+ */
3676
+ resetRefreshStats() {
3677
+ this.refreshAdapterEvents = {};
3678
+ this.refreshApiCallEventStats = {
3679
+ [REFRESH_APEX_KEY]: 0,
3680
+ [REFRESH_UIAPI_KEY]: 0,
3681
+ [SUPPORTED_KEY]: 0,
3682
+ [UNSUPPORTED_KEY]: 0,
3753
3683
  };
3684
+ this.lastRefreshApiCall = null;
3754
3685
  }
3755
3686
  /**
3756
- * For performance reasons (fear of over-fetching), we only want to predict single requests which either are not
3757
- * part of a batch request OR specify optional parameters (thus requiring data differing from that of a batch
3758
- * request).
3687
+ * W-7801618
3688
+ * Counter for occurrences where the incoming record to be merged has a different apiName.
3689
+ * Dynamically generated metric, stored in an {@link RecordApiNameChangeCounters} object.
3759
3690
  *
3760
- * ADG currently handles the batching of getRelatedListInfo -> getRelatedListInfoBatch
3761
- * https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListInfoBatchReducer.js
3691
+ * @param context The transaction context.
3762
3692
  *
3763
- * @param unfilteredRequests
3764
- * @returns GetRelatedListInfoRequest[]
3765
- * @override
3693
+ * Note: Short-lived metric candidate, remove at the end of 230
3766
3694
  */
3767
- reduce(unfilteredRequests) {
3768
- // using batch versions of request, map keys of parent record to values of related lists
3769
- const batchRequests = unfilteredRequests.filter((entry) => entry.request.adapterName === GET_RELATED_LIST_INFO_BATCH_ADAPTER_NAME).reduce((result, entry) => {
3770
- // pull batch request parameters that correspond to single request parameters
3771
- const { parentObjectApiName, relatedListNames, recordTypeId } = entry.request.config;
3772
- // key based off of parent object and its type
3773
- const key = `${parentObjectApiName}_${recordTypeId}`;
3774
- // make sure an entry exists for the record
3775
- const relatedListIds = result.get(key) || new Set();
3776
- result.set(key, relatedListIds);
3777
- // add each related list to values for the record
3778
- relatedListNames.forEach((list) => relatedListIds.add(list));
3779
- return result;
3780
- }, new Map());
3781
- // return only the single requests that will NOT be covered by a batch request
3782
- return this.filterRequests(unfilteredRequests).filter((entry) => {
3783
- const config = entry.request.config;
3784
- if (!this.isRequiredOnlyConfig(config, true)) {
3785
- // requested data is customized beyond default; batch may not cover, so keep single request
3786
- return true;
3787
- }
3788
- // key based off of parent object and its type
3789
- const key = `${config.parentObjectApiName}_${config.recordTypeId}`;
3790
- // keep only the lists that are not batched
3791
- const batchLists = batchRequests.get(key);
3792
- return !(batchLists && batchLists.has(config.relatedListId));
3793
- });
3695
+ incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
3696
+ let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
3697
+ if (apiNameChangeCounter === undefined) {
3698
+ apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
3699
+ this.recordApiNameChangeCounters[existingApiName] = apiNameChangeCounter;
3700
+ }
3701
+ apiNameChangeCounter.increment(1);
3794
3702
  }
3795
3703
  /**
3796
- * Return true if the request config doesn't specify values for any of the optional parameters.
3704
+ * W-8620679
3705
+ * Increment the counter for an UnfulfilledSnapshotError coming from luvio
3797
3706
  *
3798
- * @param config
3799
- * @param ignoreRecordType
3707
+ * @param context The transaction context.
3800
3708
  */
3801
- isRequiredOnlyConfig(config, ignoreRecordType = false) {
3802
- return ((ignoreRecordType || config.recordTypeId === undefined) &&
3803
- (config.fields || []).length === 0 &&
3804
- (config.optionalFields || []).length === 0 &&
3805
- config.restrictColumnsToLayout !== false // default is true
3806
- );
3709
+ incrementAdapterRequestErrorCount(context) {
3710
+ // We are consolidating all apex adapter instrumentation calls under a single key
3711
+ const adapterName = normalizeAdapterName(context.adapterName);
3712
+ let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
3713
+ if (adapterRequestErrorCounter === undefined) {
3714
+ adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
3715
+ this.adapterUnfulfilledErrorCounters[adapterName] = adapterRequestErrorCounter;
3716
+ }
3717
+ adapterRequestErrorCounter.increment(1);
3718
+ totalAdapterErrorMetric.increment(1);
3807
3719
  }
3808
3720
  }
3809
-
3810
- const GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME = 'getRelatedListRecordsBatch';
3811
- class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrategy {
3812
- constructor() {
3813
- super(...arguments);
3814
- this.adapterName = GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME;
3815
- this.adapterFactory = getRelatedListRecordsBatchAdapterFactory;
3816
- }
3817
- buildConcreteRequest(similarRequest, context) {
3818
- return {
3819
- ...similarRequest,
3820
- config: {
3821
- ...similarRequest.config,
3822
- parentRecordId: context.recordId,
3823
- },
3824
- };
3825
- }
3826
- transformForSaveSimilarRequest(request) {
3827
- return this.transformForSave({
3828
- ...request,
3829
- config: {
3830
- ...request.config,
3831
- parentRecordId: '*',
3832
- },
3833
- });
3721
+ function createMetricsKey(owner, name, unit) {
3722
+ let metricName = name;
3723
+ if (unit) {
3724
+ metricName = metricName + '.' + unit;
3834
3725
  }
3835
- isContextDependent(context, request) {
3836
- return context.recordId === request.config.parentRecordId;
3726
+ return {
3727
+ get() {
3728
+ return { owner: owner, name: metricName };
3729
+ },
3730
+ };
3731
+ }
3732
+ /**
3733
+ * Returns whether adapter is an Apex one or not.
3734
+ * @param adapterName The name of the adapter.
3735
+ */
3736
+ function isApexAdapter(adapterName) {
3737
+ return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
3738
+ }
3739
+ /**
3740
+ * Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
3741
+ * API family, if supplied. Example: `UiApi.getRecord`.
3742
+ *
3743
+ * Note: If you are adding additional logging that can come from getApex adapter contexts that provide
3744
+ * the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
3745
+ * ensure to call this method to normalize all logging to 'getApex'. This
3746
+ * is because Argus has a 50k key cardinality limit. More context: W-8379680.
3747
+ *
3748
+ * @param adapterName The name of the adapter.
3749
+ * @param apiFamily The API family of the adapter.
3750
+ */
3751
+ function normalizeAdapterName(adapterName, apiFamily) {
3752
+ if (isApexAdapter(adapterName)) {
3753
+ return NORMALIZED_APEX_ADAPTER_NAME;
3837
3754
  }
3838
- /**
3839
- * Can combine two seperate batch requests if the parentRecordId is the same.
3840
- * @param reqA The first GetRelatedListRecordsBatchConfig.
3841
- * @param reqB The first GetRelatedListRecordsBatchConfig.
3842
- * @returns true if the requests can be combined, otherwise false.
3843
- */
3844
- canCombine(reqA, reqB) {
3845
- return reqA.parentRecordId === reqB.parentRecordId;
3755
+ return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
3756
+ }
3757
+ const timerMetricTracker = create(null);
3758
+ /**
3759
+ * Calls instrumentation/service telemetry timer
3760
+ * @param name Name of the metric
3761
+ * @param duration number to update backing percentile histogram, negative numbers ignored
3762
+ */
3763
+ function updateTimerMetric(name, duration) {
3764
+ let metric = timerMetricTracker[name];
3765
+ if (metric === undefined) {
3766
+ metric = timer(createMetricsKey(NAMESPACE, name));
3767
+ timerMetricTracker[name] = metric;
3846
3768
  }
3847
- /**
3848
- * Merge the relatedListParameters together between two combinable batch requests.
3849
- * @param reqA The first GetRelatedListRecordsBatchConfig.
3850
- * @param reqB The first GetRelatedListRecordsBatchConfig.
3851
- * @returns The combined request.
3852
- */
3853
- combineRequests(reqA, reqB) {
3854
- const relatedListParametersMap = new Set(reqA.relatedListParameters.map((relatedListParameter) => {
3855
- return stableJSONStringify(relatedListParameter);
3856
- }));
3857
- const reqBRelatedListParametersToAdd = reqB.relatedListParameters.filter((relatedListParameter) => {
3858
- return !relatedListParametersMap.has(stableJSONStringify(relatedListParameter));
3859
- });
3860
- reqA.relatedListParameters = reqA.relatedListParameters.concat(reqBRelatedListParametersToAdd);
3861
- return reqA;
3769
+ timerMetricAddDuration(metric, duration);
3770
+ }
3771
+ function timerMetricAddDuration(timer, duration) {
3772
+ // Guard against negative values since it causes error to be thrown by MetricsService
3773
+ if (duration >= 0) {
3774
+ timer.addDuration(duration);
3862
3775
  }
3863
3776
  }
3864
-
3865
- class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
3866
- constructor() {
3867
- super(...arguments);
3868
- this.adapterName = 'getRelatedListRecords';
3869
- this.adapterFactory = getRelatedListRecordsAdapterFactory;
3777
+ /**
3778
+ * W-10315098
3779
+ * Increments the counter associated with the request response. Counts are bucketed by status.
3780
+ */
3781
+ const requestResponseMetricTracker = create(null);
3782
+ function incrementRequestResponseCount(cb) {
3783
+ const status = cb().status;
3784
+ let metric = requestResponseMetricTracker[status];
3785
+ if (metric === undefined) {
3786
+ metric = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, NETWORK_ADAPTER_RESPONSE_METRIC_NAME, `${status.valueOf()}`));
3787
+ requestResponseMetricTracker[status] = metric;
3870
3788
  }
3871
- buildConcreteRequest(similarRequest, context) {
3872
- return {
3873
- ...similarRequest,
3874
- config: {
3875
- ...similarRequest.config,
3876
- parentRecordId: context.recordId,
3789
+ metric.increment();
3790
+ }
3791
+ function logObjectInfoChanged() {
3792
+ logObjectInfoChanged$1();
3793
+ }
3794
+ /**
3795
+ * Create a new instrumentation cache stats and return it.
3796
+ *
3797
+ * @param name The cache logger name.
3798
+ */
3799
+ function registerLdsCacheStats(name) {
3800
+ return registerCacheStats(`${NAMESPACE}:${name}`);
3801
+ }
3802
+ /**
3803
+ * Add or overwrite hooks that require aura implementations
3804
+ */
3805
+ function setAuraInstrumentationHooks() {
3806
+ instrument({
3807
+ recordConflictsResolved: (serverRequestCount) => {
3808
+ // Ignore 0 values which can originate from ADS bridge
3809
+ if (serverRequestCount > 0) {
3810
+ updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
3811
+ }
3812
+ },
3813
+ nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
3814
+ const metricName = `merge-null-dv-count.${fieldType}`;
3815
+ if (fieldType === 'scalar') {
3816
+ incrementCounterMetric(`${metricName}.${areValuesEqual}`);
3817
+ }
3818
+ else {
3819
+ incrementCounterMetric(metricName);
3820
+ }
3821
+ },
3822
+ getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
3823
+ getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
3824
+ notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
3825
+ notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
3826
+ recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
3827
+ weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
3828
+ getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
3829
+ });
3830
+ withRegistration('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
3831
+ instrument$1({
3832
+ logCrud: logCRUDLightningInteraction,
3833
+ networkResponse: incrementRequestResponseCount,
3834
+ });
3835
+ instrument$2({
3836
+ error: logError,
3837
+ });
3838
+ instrument$3({
3839
+ refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
3840
+ instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
3841
+ });
3842
+ instrument$4({
3843
+ timerMetricAddDuration: updateTimerMetric,
3844
+ });
3845
+ // Our getRecord through aggregate-ui CRUD logging has moved
3846
+ // to lds-network-adapter. We still need to respect the
3847
+ // orgs environment setting
3848
+ if (forceRecordTransactionsDisabled$1 === false) {
3849
+ ldsNetworkAdapterInstrument({
3850
+ getRecordAggregateResolve: (cb) => {
3851
+ const { recordId, apiName } = cb();
3852
+ logCRUDLightningInteraction('read', {
3853
+ recordId,
3854
+ recordType: apiName,
3855
+ state: 'SUCCESS',
3856
+ });
3877
3857
  },
3878
- };
3879
- }
3880
- /**
3881
- *
3882
- * This method returns GetRelatedListRecordsRequest[] that won't be part of a batch request.
3883
- *
3884
- * ADG currently handles the batching of GetRelatedListRecords -> GetRelatedListRecordsBatch
3885
- * https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListRecordsBatchReducer.js
3886
- *
3887
- * For performance reasons (fear to overfetch), we only check that the Single relatedListId is not present in any of the Batch requests,
3888
- * but we don't check for any other parameters.
3889
- *
3890
- * @param unfilteredRequests All of the request available for predictions.
3891
- * @returns GetRelatedListRecordsRequest[] That should be a prediction.
3892
- */
3893
- reduce(unfilteredRequests) {
3894
- // Batch requests by [parentRecordId]->[RelatedListIds]
3895
- const batchRequests = unfilteredRequests.filter((entry) => entry.request.adapterName === GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME).reduce((acc, entry) => {
3896
- // required properties, enforced by adapter typecheck
3897
- const { parentRecordId, relatedListParameters } = entry.request.config;
3898
- const existingRlSet = acc.get(parentRecordId) || new Set();
3899
- // relatedListId enforced by adapter typecheck
3900
- relatedListParameters.forEach((rlParam) => existingRlSet.add(rlParam.relatedListId));
3901
- acc.set(parentRecordId, existingRlSet);
3902
- return acc;
3903
- }, new Map());
3904
- const singleRequests = unfilteredRequests.filter((entry) => entry.request.adapterName === this.adapterName);
3905
- return singleRequests.filter((entry) => {
3906
- // required props enforced by adapter typecheck
3907
- const { parentRecordId, relatedListId } = entry.request.config;
3908
- const batchForParentRecordId = batchRequests.get(parentRecordId);
3909
- return !(batchForParentRecordId && batchForParentRecordId.has(relatedListId));
3910
- });
3911
- }
3912
- transformForSaveSimilarRequest(request) {
3913
- return this.transformForSave({
3914
- ...request,
3915
- config: {
3916
- ...request.config,
3917
- parentRecordId: '*',
3858
+ getRecordAggregateReject: (cb) => {
3859
+ const recordId = cb();
3860
+ logCRUDLightningInteraction('read', {
3861
+ recordId,
3862
+ state: 'ERROR',
3863
+ });
3918
3864
  },
3919
3865
  });
3920
3866
  }
3921
- isContextDependent(context, request) {
3922
- return context.recordId === request.config.parentRecordId;
3923
- }
3867
+ withRegistration('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
3924
3868
  }
3925
-
3926
- const APEX_RESOURCE_CONTEXT = {
3927
- ...DEFAULT_RESOURCE_CONTEXT,
3928
- sourceContext: {
3929
- ...DEFAULT_RESOURCE_CONTEXT.sourceContext,
3930
- // We don't want to override anything for Apex, it is not part
3931
- // of UiApi, and it can cause undesired behavior.
3932
- actionConfig: undefined,
3933
- },
3934
- };
3935
- function getApexPdlFactory(luvio) {
3936
- return ({ invokerParams, config }, requestContext) => {
3937
- return GetApexWireAdapterFactory(luvio, invokerParams)(config, requestContext);
3938
- };
3869
+ /**
3870
+ * Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
3871
+ *
3872
+ * @param luvio The Luvio instance to instrument.
3873
+ * @param store The InMemoryStore to instrument.
3874
+ */
3875
+ function setupInstrumentation(luvio, store) {
3876
+ setupInstrumentation$1(luvio, store);
3877
+ setAuraInstrumentationHooks();
3939
3878
  }
3940
- class GetApexRequestStrategy extends LuvioAdapterRequestStrategy {
3941
- constructor() {
3942
- super(...arguments);
3943
- this.adapterName = 'getApex';
3944
- this.adapterFactory = getApexPdlFactory;
3945
- }
3946
- buildConcreteRequest(similarRequest) {
3947
- return similarRequest;
3948
- }
3949
- execute(config, _requestContext) {
3950
- return super.execute(config, APEX_RESOURCE_CONTEXT);
3951
- }
3879
+ /**
3880
+ * Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
3881
+ * disable all crud operations if it is on.
3882
+ * @param eventSource - Source of the logging event.
3883
+ * @param attributes - Free form object of attributes to log.
3884
+ */
3885
+ function logCRUDLightningInteraction(eventSource, attributes) {
3886
+ interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
3952
3887
  }
3888
+ const instrumentation = new Instrumentation();
3953
3889
 
3954
- const GET_LIST_INFO_BY_NAME_ADAPTER_NAME = 'getListInfoByName';
3955
- class GetListInfoByNameRequestStrategy extends LuvioAdapterRequestStrategy {
3956
- constructor() {
3957
- super(...arguments);
3958
- this.adapterName = GET_LIST_INFO_BY_NAME_ADAPTER_NAME;
3959
- this.adapterFactory = getListInfoByNameAdapterFactory;
3890
+ class ApplicationPredictivePrefetcher {
3891
+ constructor(context, repository, requestRunner) {
3892
+ this.repository = repository;
3893
+ this.requestRunner = requestRunner;
3894
+ this.isRecording = false;
3895
+ this.totalRequestCount = 0;
3896
+ this.queuedPredictionRequests = [];
3897
+ this._context = context;
3898
+ this.page = this.getPage();
3960
3899
  }
3961
- buildConcreteRequest(similarRequest) {
3962
- return similarRequest;
3900
+ set context(value) {
3901
+ this._context = value;
3902
+ this.page = this.getPage();
3963
3903
  }
3964
- transformForSave(request) {
3965
- return {
3966
- ...request,
3967
- config: {
3968
- ...request.config,
3969
- // (!): if we are saving this request is because the adapter already verified is valid.
3970
- objectApiName: coerceObjectId(request.config.objectApiName),
3971
- },
3972
- };
3904
+ get context() {
3905
+ return this._context;
3973
3906
  }
3974
- canCombine(reqA, reqB) {
3975
- return (reqA.objectApiName === reqB.objectApiName &&
3976
- reqA.listViewApiName === reqB.listViewApiName);
3907
+ async stopRecording() {
3908
+ this.isRecording = false;
3909
+ this.totalRequestCount = 0;
3910
+ await this.repository.flushRequestsToStorage();
3977
3911
  }
3978
- combineRequests(reqA, _reqB) {
3979
- return reqA;
3912
+ startRecording() {
3913
+ this.isRecording = true;
3914
+ this.repository.markPageStart();
3915
+ this.repository.clearRequestBuffer();
3980
3916
  }
3981
- }
3982
-
3983
- const GET_LIST_INFOS_BY_OBJECT_NAME_ADAPTER_NAME = 'getListInfosByObjectName';
3984
- class GetListInfosByObjectNameRequestStrategy extends LuvioAdapterRequestStrategy {
3985
- constructor() {
3986
- super(...arguments);
3987
- this.adapterName = GET_LIST_INFOS_BY_OBJECT_NAME_ADAPTER_NAME;
3988
- this.adapterFactory = getListInfosByObjectNameAdapterFactory;
3917
+ saveRequest(request) {
3918
+ if (!this.isRecording) {
3919
+ return;
3920
+ }
3921
+ executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_SAVE_REQUEST, (_act) => {
3922
+ if (this.page.supportsRequest(request)) {
3923
+ const saveBuckets = this.page.buildSaveRequestData(request);
3924
+ saveBuckets.forEach((saveBucket) => {
3925
+ const { request: requestToSave, context } = saveBucket;
3926
+ // No need to differentiate from predictions requests because these
3927
+ // are made from the adapters factory, which are not prediction aware.
3928
+ this.repository.saveRequest(context, requestToSave);
3929
+ });
3930
+ }
3931
+ return Promise.resolve().then();
3932
+ }, PDL_EXECUTE_ASYNC_OPTIONS);
3989
3933
  }
3990
- buildConcreteRequest(similarRequest) {
3991
- return similarRequest;
3934
+ async predict() {
3935
+ const alwaysRequests = this.page.getAlwaysRunRequests();
3936
+ const similarPageRequests = await this.getSimilarPageRequests();
3937
+ const exactPageRequests = await this.getExactPageRequest();
3938
+ // Always requests can't be reduced in - Some of them are essential to keep the page rendering at the beginning.
3939
+ const reducedRequests = this.requestRunner
3940
+ .reduceRequests([...exactPageRequests, ...similarPageRequests])
3941
+ .map((entry) => entry.request);
3942
+ const predictedRequests = [...alwaysRequests, ...reducedRequests];
3943
+ this.queuedPredictionRequests.push(...predictedRequests);
3944
+ this.totalRequestCount = predictedRequests.length;
3945
+ return Promise.all(predictedRequests.map((request) => this.requestRunner.runRequest(request))).then();
3992
3946
  }
3993
- transformForSave(request) {
3947
+ getPredictionSummary() {
3948
+ const exactPageRequests = this.repository.getPageRequests(this.context) || [];
3949
+ const similarPageRequests = this.page.similarContext !== undefined
3950
+ ? this.repository.getPageRequests(this.page.similarContext)
3951
+ : [];
3994
3952
  return {
3995
- ...request,
3996
- config: {
3997
- ...request.config,
3998
- // (!): if we are saving this request is because the adapter already verified is valid.
3999
- objectApiName: coerceObjectId(request.config.objectApiName),
4000
- },
3953
+ exact: exactPageRequests.length,
3954
+ similar: similarPageRequests.length,
3955
+ totalRequestCount: this.totalRequestCount,
4001
3956
  };
4002
3957
  }
4003
- canCombine(reqA, reqB) {
4004
- return (reqA.objectApiName === reqB.objectApiName &&
4005
- reqA.q === reqB.q &&
4006
- reqA.recentListsOnly === reqB.recentListsOnly &&
4007
- reqA.pageSize === reqB.pageSize);
3958
+ hasPredictions() {
3959
+ const summary = this.getPredictionSummary();
3960
+ return summary.exact > 0 || summary.similar > 0;
4008
3961
  }
4009
- combineRequests(reqA, _reqB) {
4010
- return reqA;
3962
+ getSimilarPageRequests() {
3963
+ let resolvedSimilarPageRequests = [];
3964
+ if (this.page.similarContext !== undefined) {
3965
+ const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
3966
+ if (similarPageRequests !== undefined) {
3967
+ resolvedSimilarPageRequests = similarPageRequests.map((entry) => {
3968
+ return {
3969
+ ...entry,
3970
+ request: this.page.resolveSimilarRequest(entry.request),
3971
+ };
3972
+ });
3973
+ }
3974
+ }
3975
+ return resolvedSimilarPageRequests;
3976
+ }
3977
+ getExactPageRequest() {
3978
+ return this.repository.getPageRequests(this.context) || [];
4011
3979
  }
4012
3980
  }
4013
3981
 
4014
- const GET_LIST_RECORDS_BY_NAME_ADAPTER_NAME = 'getListRecordsByName';
4015
- class GetListRecordsByNameRequestStrategy extends LuvioAdapterRequestStrategy {
4016
- constructor() {
4017
- super(...arguments);
4018
- this.adapterName = GET_LIST_RECORDS_BY_NAME_ADAPTER_NAME;
4019
- this.adapterFactory = getListRecordsByNameAdapterFactory;
3982
+ /**
3983
+ * Runs a list of requests with a specified concurrency limit.
3984
+ *
3985
+ * @template Request - The type of the requests being processed.
3986
+ * @param {RequestEntry<Request>[]} requests - An array of request entries to be processed in the array order.
3987
+ * @param {RequestRunner<Request>} runner - The runner instance responsible for executing the requests.
3988
+ * @param {number} concurrentRequestsLimit - The maximum number of concurrent requests allowed.
3989
+ * @param {number} pageStartTime - The start time of the page load, used to calculate the time elapsed since the page starts loading.
3990
+ * @returns {Promise<void>} A promise that resolves when all requests have been processed.
3991
+ *
3992
+ * This function manages a queue of pending requests and processes them with a concurrency limit.
3993
+ * Requests are only processed if their `requestTime` is less than the time elapsed since `pageStartTime`.
3994
+ */
3995
+ async function runRequestsWithLimit(requests, runner, concurrentRequestsLimit, pageStartTime) {
3996
+ // queue for pending prediction requests
3997
+ const requestQueue = [...requests];
3998
+ // Function to process the next request in the queue
3999
+ const processNextRequest = async (verifyPastTime = true) => {
4000
+ const timeInWaterfall = Date.now() - pageStartTime;
4001
+ while (requestQueue.length > 0 &&
4002
+ verifyPastTime &&
4003
+ requestQueue[0].requestMetadata.requestTime <= timeInWaterfall) {
4004
+ requestQueue.shift();
4005
+ }
4006
+ if (requestQueue.length > 0) {
4007
+ // (!) requestQueue will always have at least one element ensured by the above check.
4008
+ const nextRequest = requestQueue.shift();
4009
+ try {
4010
+ // Run the request and wait for it to complete
4011
+ await runner.runRequest(nextRequest.request);
4012
+ }
4013
+ finally {
4014
+ await processNextRequest();
4015
+ }
4016
+ }
4017
+ };
4018
+ // Start processing requests up to concurrentRequestsLimit
4019
+ const initialRequests = Math.min(concurrentRequestsLimit, requestQueue.length);
4020
+ const promises = [];
4021
+ for (let i = 0; i < initialRequests; i++) {
4022
+ // Initial requests should always execute, without verifying if they are past due.
4023
+ // Reasoning:
4024
+ // It may be that one of the alwaysRequest (with 0 as start time) that is reduced
4025
+ // with the regular requests to make these to have 0 as the initial time in the waterfall.
4026
+ // Because predictions are behind an await (see W-16139321), it could be that when this code is evaluated
4027
+ // is already past time for the request.
4028
+ promises.push(processNextRequest(false));
4020
4029
  }
4021
- buildConcreteRequest(similarRequest) {
4022
- return similarRequest;
4030
+ // Wait for all initial requests to complete
4031
+ await Promise.all(promises);
4032
+ }
4033
+
4034
+ function isBoxcarableRequest({ request: { adapterName } }, requestStrategyManager) {
4035
+ const strategy = requestStrategyManager.get(adapterName);
4036
+ return strategy === undefined || strategy.isBoxcarable;
4037
+ }
4038
+ function predictNonBoxcarableRequest(nonBoxcaredPredictions, requestRunner) {
4039
+ const reducedPredictions = requestRunner.reduceRequests(nonBoxcaredPredictions);
4040
+ reducedPredictions.map((request) => requestRunner.runRequest(request.request));
4041
+ }
4042
+ class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
4043
+ constructor(context, repository, requestRunner,
4044
+ // These strategies need to be in sync with the "predictiveDataLoadCapable" list
4045
+ // from scripts/lds-uiapi-plugin.js
4046
+ requestStrategyManager, options) {
4047
+ super(context, repository, requestRunner);
4048
+ this.options = options;
4049
+ this.requestStrategyManager = requestStrategyManager;
4050
+ this.page = this.getPage();
4023
4051
  }
4024
- transformForSave(request) {
4025
- if (request.config.fields === undefined && request.config.optionalFields === undefined) {
4026
- return request;
4052
+ getPage() {
4053
+ if (RecordHomePage.handlesContext(this.context)) {
4054
+ return new RecordHomePage(this.context, this.requestStrategyManager, this.options);
4027
4055
  }
4028
- let fields = request.config.fields || [];
4029
- let optionalFields = request.config.optionalFields || [];
4030
- return {
4031
- ...request,
4032
- config: {
4033
- ...request.config,
4034
- fields: [],
4035
- optionalFields: Array.from(new Set([...fields, ...optionalFields])),
4036
- },
4037
- };
4056
+ else if (ObjectHomePage.handlesContext(this.context)) {
4057
+ return new ObjectHomePage(this.context, this.requestStrategyManager, this.options);
4058
+ }
4059
+ return new LexDefaultPage(this.context);
4038
4060
  }
4039
- canCombine(reqA, reqB) {
4040
- return (reqA.objectApiName === reqB.objectApiName &&
4041
- reqA.listViewApiName === reqB.listViewApiName &&
4042
- reqA.pageSize === reqB.pageSize &&
4043
- reqA.searchTerm === reqB.searchTerm &&
4044
- reqA.sortBy === reqB.sortBy &&
4045
- reqA.where === reqB.where);
4061
+ getAllPageRequests() {
4062
+ const exactPageRequests = this.getExactPageRequest();
4063
+ let similarPageRequests = this.getSimilarPageRequests();
4064
+ if (exactPageRequests.length > 0 && this.options.useExactMatchesPlus === true) {
4065
+ similarPageRequests = similarPageRequests.filter((requestEntry) => {
4066
+ const strategy = this.requestStrategyManager.get(requestEntry.request.adapterName);
4067
+ return strategy && strategy.onlySavedInSimilar;
4068
+ });
4069
+ }
4070
+ return [...exactPageRequests, ...similarPageRequests];
4046
4071
  }
4047
- combineRequests(reqA, reqB) {
4048
- return {
4049
- ...reqA,
4050
- optionalFields: Array.from(new Set([...(reqA.optionalFields || []), ...(reqB.optionalFields || [])])),
4051
- };
4072
+ async predict() {
4073
+ const alwaysRequests = this.page.getAlwaysRunRequests();
4074
+ const pageRequests = this.getAllPageRequests();
4075
+ // IMPORTANT: Because there's no way to diferentiate a cmpDef prediction from the page
4076
+ // requesting the cmpDef, we need to predict cmpDefs before we start watching
4077
+ // for predictions in the page. Having this code after an
4078
+ // await will make the predictions to be saved as predictions too.
4079
+ predictNonBoxcarableRequest(pageRequests.filter((r) => !isBoxcarableRequest(r, this.requestStrategyManager)), this.requestRunner);
4080
+ const alwaysRequestEntries = alwaysRequests.map((request) => {
4081
+ return {
4082
+ request,
4083
+ requestMetadata: { requestTime: 0 }, // ensures always requests are executed, and executed first.
4084
+ };
4085
+ });
4086
+ const boxcarablePredictions = pageRequests.filter((r) => isBoxcarableRequest(r, this.requestStrategyManager));
4087
+ const reducedPredictions = this.page.shouldReduceAlwaysRequestsWithPredictions()
4088
+ ? this.requestRunner.reduceRequests([...boxcarablePredictions, ...alwaysRequestEntries])
4089
+ : this.requestRunner.reduceRequests(boxcarablePredictions);
4090
+ const predictedRequestsWithLimit = (this.page.shouldExecuteAlwaysRequestByThemself()
4091
+ ? [...alwaysRequestEntries, ...reducedPredictions]
4092
+ : reducedPredictions)
4093
+ // Sorting in order requested
4094
+ .sort((a, b) => a.requestMetadata.requestTime - b.requestMetadata.requestTime);
4095
+ this.totalRequestCount = predictedRequestsWithLimit.length;
4096
+ await runRequestsWithLimit(predictedRequestsWithLimit, this.requestRunner, this.options.inflightRequestLimit,
4097
+ // `this.repository.pageStartTime` would be the correct here,
4098
+ // but when doing predict+watch, it could be (set in watch)
4099
+ // repository.startTime is not set yet, Date.now() is a better alternative,
4100
+ // that is correct, and works on both cases.
4101
+ Date.now());
4052
4102
  }
4053
4103
  }
4054
4104
 
4055
- const GET_LIST_OBJECT_INFO_ADAPTER_NAME = 'getListObjectInfo';
4056
- class GetListObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
4057
- constructor() {
4058
- super(...arguments);
4059
- this.adapterName = GET_LIST_OBJECT_INFO_ADAPTER_NAME;
4060
- this.adapterFactory = getListObjectInfoAdapterFactory;
4061
- }
4062
- buildConcreteRequest(similarRequest) {
4063
- return similarRequest;
4064
- }
4065
- transformForSave(request) {
4066
- return {
4067
- ...request,
4068
- config: {
4069
- ...request.config,
4070
- // (!): if we are saving this request is because the adapter already verified is valid.
4071
- objectApiName: coerceObjectId(request.config.objectApiName),
4072
- },
4073
- };
4105
+ class LexRequestRunner {
4106
+ constructor(requestStrategyManager) {
4107
+ this.requestStrategyManager = requestStrategyManager;
4108
+ this.requestStrategyManager = requestStrategyManager;
4074
4109
  }
4075
- canCombine(reqA, reqB) {
4076
- return reqA.objectApiName === reqB.objectApiName;
4110
+ reduceRequests(requests) {
4111
+ return this.requestStrategyManager
4112
+ .getAll()
4113
+ .map((strategy) => strategy.reduce(requests))
4114
+ .flat();
4077
4115
  }
4078
- combineRequests(reqA, _reqB) {
4079
- return reqA;
4116
+ runRequest(request) {
4117
+ const strategy = this.requestStrategyManager.get(request.adapterName);
4118
+ if (strategy) {
4119
+ return Promise.resolve(strategy.execute(request.config)).then();
4120
+ }
4121
+ return Promise.resolve(undefined);
4080
4122
  }
4081
4123
  }
4082
4124
 
@@ -4104,18 +4146,19 @@ const DEFAULT_STORAGE_OPTIONS = {
4104
4146
  version: 3,
4105
4147
  };
4106
4148
  function buildAuraPrefetchStorage(options = {}) {
4149
+ const { onPredictionsReadyCallback, ...storageOptions } = options;
4107
4150
  const auraStorage = createStorage({
4108
4151
  ...DEFAULT_STORAGE_OPTIONS,
4109
- ...options,
4152
+ ...storageOptions,
4110
4153
  });
4111
4154
  const inMemoryStorage = new InMemoryPrefetchStorage();
4112
4155
  if (auraStorage === null) {
4113
4156
  return inMemoryStorage;
4114
4157
  }
4115
- return new AuraPrefetchStorage(auraStorage, inMemoryStorage);
4158
+ return new AuraPrefetchStorage(auraStorage, inMemoryStorage, onPredictionsReadyCallback);
4116
4159
  }
4117
4160
  class AuraPrefetchStorage {
4118
- constructor(auraStorage, inMemoryStorage) {
4161
+ constructor(auraStorage, inMemoryStorage, onPredictionsReadyCallback = () => { }) {
4119
4162
  this.auraStorage = auraStorage;
4120
4163
  this.inMemoryStorage = inMemoryStorage;
4121
4164
  /**
@@ -4132,6 +4175,7 @@ class AuraPrefetchStorage {
4132
4175
  */
4133
4176
  auraStorage.getAll().then((results) => {
4134
4177
  keys(results).forEach((key) => this.inMemoryStorage.set(key, results[key]));
4178
+ onPredictionsReadyCallback();
4135
4179
  });
4136
4180
  }
4137
4181
  set(key, value) {
@@ -4205,7 +4249,7 @@ function getEnvironmentSetting(name) {
4205
4249
  }
4206
4250
  return undefined;
4207
4251
  }
4208
- // version: 1.314.0-022b0c843b
4252
+ // version: 1.316.0-4453f1e2b6
4209
4253
 
4210
4254
  const forceRecordTransactionsDisabled = getEnvironmentSetting(EnvironmentSettings.ForceRecordTransactionsDisabled);
4211
4255
  //TODO: Some duplication here that can be most likely moved to a util class
@@ -4531,6 +4575,98 @@ function setupMetadataWatcher(luvio) {
4531
4575
  });
4532
4576
  }
4533
4577
 
4578
+ class RequestStrategyManager {
4579
+ constructor(requestStrategies = []) {
4580
+ this.requestStrategies = [];
4581
+ this.requestStrategiesByAdapterName = new Map();
4582
+ this.register(requestStrategies);
4583
+ }
4584
+ setRequestStrategy(requestStrategy) {
4585
+ const adapterName = requestStrategy.adapterName;
4586
+ if (!this.requestStrategiesByAdapterName.has(adapterName)) {
4587
+ this.requestStrategiesByAdapterName.set(adapterName, requestStrategy);
4588
+ this.requestStrategies.push(requestStrategy);
4589
+ return true;
4590
+ }
4591
+ if (process.env.NODE_ENV !== 'production') {
4592
+ throw new Error(`Registered an already existing request strategy: ${adapterName}`);
4593
+ }
4594
+ return false;
4595
+ }
4596
+ deleteRequestStrategy(requestStrategy) {
4597
+ const adapterName = requestStrategy.adapterName;
4598
+ if (this.requestStrategiesByAdapterName.has(adapterName)) {
4599
+ this.requestStrategiesByAdapterName.delete(adapterName);
4600
+ const indexToRemove = this.requestStrategies.findIndex((strategy) => strategy === requestStrategy);
4601
+ this.requestStrategies.splice(indexToRemove, 1);
4602
+ return true;
4603
+ }
4604
+ if (process.env.NODE_ENV !== 'production') {
4605
+ throw new Error(`Could not find existing request strategy to unregister: ${adapterName}`);
4606
+ }
4607
+ return false;
4608
+ }
4609
+ register(requestStrategies) {
4610
+ return requestStrategies.map((requestStrategy) => this.setRequestStrategy(requestStrategy));
4611
+ }
4612
+ unregister(requestStrategies) {
4613
+ return requestStrategies.map((requestStrategy) => this.deleteRequestStrategy(requestStrategy));
4614
+ }
4615
+ get(adapterName) {
4616
+ return this.requestStrategiesByAdapterName.get(adapterName);
4617
+ }
4618
+ getAll() {
4619
+ return this.requestStrategies;
4620
+ }
4621
+ }
4622
+
4623
+ /**
4624
+ * Manages the state of predictions and notifies registered callbacks when predictions are ready.
4625
+ */
4626
+ class PredictionsReadyManager {
4627
+ constructor() {
4628
+ /**
4629
+ * Array of callbacks to be executed when predictions are ready.
4630
+ * @private
4631
+ */
4632
+ this.predictionsReadyCallbacks = [];
4633
+ /**
4634
+ * Indicates whether the predictions are ready.
4635
+ * @private
4636
+ */
4637
+ this.predictionsReady = false;
4638
+ }
4639
+ /**
4640
+ * Registers a callback to be called when predictions are ready.
4641
+ * If predictions are already ready, the callback is invoked immediately.
4642
+ *
4643
+ * @param {() => void} callback - The callback function to be executed when predictions are ready.
4644
+ */
4645
+ whenPredictionsReady(callback) {
4646
+ if (this.predictionsReady) {
4647
+ callback();
4648
+ }
4649
+ else {
4650
+ this.predictionsReadyCallbacks.push(callback);
4651
+ }
4652
+ }
4653
+ /**
4654
+ * Notifies that predictions are ready by invoking all registered callbacks.
4655
+ */
4656
+ notifyPredictionsReady() {
4657
+ this.predictionsReady = true;
4658
+ this.predictionsReadyCallbacks.forEach((cb) => cb());
4659
+ }
4660
+ /**
4661
+ * Retrieves the list of callbacks registered to be executed when predictions are ready.
4662
+ *
4663
+ * @returns {Array<() => void>} Array of callback functions.
4664
+ */
4665
+ getCallbacks() {
4666
+ return this.predictionsReadyCallbacks;
4667
+ }
4668
+ }
4669
+
4534
4670
  // This code *should* be in lds-network-adapter, but when combined with the Aura
4535
4671
  // component test workaround in lds-default-luvio it creates a circular dependecy
4536
4672
  // between lds-default-luvio and lds-network-adapter. We do the register on behalf
@@ -4567,6 +4703,29 @@ function getInflightRequestLimit() {
4567
4703
  return HARDCODED_REQUEST_LIMIT;
4568
4704
  }
4569
4705
  }
4706
+ const requestStrategyManager = new RequestStrategyManager();
4707
+ /**
4708
+ * Registers a request strategy to be utilized by PDL.
4709
+ * @param requestStrategy - {@link LexRequestStrategy} The request strategy/strategies to register.
4710
+ * @returns boolean | boolean[] - true/false depending on registration success.
4711
+ */
4712
+ function registerRequestStrategy(...requestStrategy) {
4713
+ const result = requestStrategyManager.register(requestStrategy);
4714
+ return result.length === 1 ? result[0] : result;
4715
+ }
4716
+ /**
4717
+ * Unregisters a request strategy to be utilized by PDL.
4718
+ * @param requestStrategy - {@link LexRequestStrategy} The request strategy to unregister.
4719
+ * @returns boolean | boolean[] - true/false depending on unregistration success.
4720
+ */
4721
+ function unregisterRequestStrategy(...requestStrategy) {
4722
+ const result = requestStrategyManager.unregister(requestStrategy);
4723
+ return result.length === 1 ? result[0] : result;
4724
+ }
4725
+ const predictionsReadyManager = new PredictionsReadyManager();
4726
+ function whenPredictionsReady(callback) {
4727
+ predictionsReadyManager.whenPredictionsReady(callback);
4728
+ }
4570
4729
  function setupPredictivePrefetcher(luvio) {
4571
4730
  const allStrategies = [
4572
4731
  new GetRecordRequestStrategy(luvio),
@@ -4587,9 +4746,11 @@ function setupPredictivePrefetcher(luvio) {
4587
4746
  new GetListInfosByObjectNameRequestStrategy(luvio),
4588
4747
  new GetListObjectInfoRequestStrategy(luvio),
4589
4748
  ];
4590
- const storage = buildAuraPrefetchStorage();
4591
- const requestRunner = new LexRequestRunner();
4592
- requestRunner.setRequestStrategies(allStrategies);
4749
+ requestStrategyManager.register(allStrategies);
4750
+ const storage = buildAuraPrefetchStorage({
4751
+ onPredictionsReadyCallback: () => predictionsReadyManager.notifyPredictionsReady(),
4752
+ });
4753
+ const requestRunner = new LexRequestRunner(requestStrategyManager);
4593
4754
  const repository = new PrefetchRepository(storage, {
4594
4755
  modifyBeforeSaveHook: (requests) => requestRunner.reduceRequests(requests),
4595
4756
  });
@@ -4601,7 +4762,7 @@ function setupPredictivePrefetcher(luvio) {
4601
4762
  inflightRequestLimit,
4602
4763
  useExactMatchesPlus,
4603
4764
  };
4604
- const prefetcher = new LexPredictivePrefetcher({ context: 'unknown' }, repository, requestRunner, allStrategies, prefetcherOptions);
4765
+ const prefetcher = new LexPredictivePrefetcher({ context: 'unknown' }, repository, requestRunner, requestStrategyManager, prefetcherOptions);
4605
4766
  registerPrefetcher(luvio, prefetcher);
4606
4767
  if (useApexPredictions.isOpen({ fallback: false })) {
4607
4768
  registerPrefetcher$1(luvio, prefetcher);
@@ -4633,6 +4794,11 @@ function loadComponentsDefStartedOverride(...args) {
4633
4794
  }
4634
4795
  return ret;
4635
4796
  }
4797
+ function saveRequestAsPrediction(request) {
4798
+ if (__lexPrefetcher !== undefined) {
4799
+ __lexPrefetcher.saveRequest(request);
4800
+ }
4801
+ }
4636
4802
  /**
4637
4803
  * @typedef {Object} RecordHomePageContext
4638
4804
  * @property {string} objectApiName - The API name of the object.
@@ -4663,6 +4829,16 @@ function buildPredictorForContext(context) {
4663
4829
  if (__lexPrefetcher === undefined) {
4664
4830
  return;
4665
4831
  }
4832
+ const currentContext = __lexPrefetcher.context;
4833
+ let isSameContext = false;
4834
+ if (currentContext && currentContext.type === 'recordPage') {
4835
+ const rhPageContext = currentContext;
4836
+ isSameContext =
4837
+ rhPageContext.actionName === context.actionName &&
4838
+ rhPageContext.objectApiName === context.objectApiName &&
4839
+ rhPageContext.recordId === context.recordId &&
4840
+ rhPageContext.type === context.type;
4841
+ }
4666
4842
  // // This chunk configures which page we're going to use to try and preload.
4667
4843
  __lexPrefetcher.context = context;
4668
4844
  return {
@@ -4691,6 +4867,9 @@ function buildPredictorForContext(context) {
4691
4867
  });
4692
4868
  },
4693
4869
  runPredictions() {
4870
+ if (isSameContext) {
4871
+ return Promise.resolve();
4872
+ }
4694
4873
  return executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_PREDICT, (_act) => __lexPrefetcher.predict(), PDL_EXECUTE_ASYNC_OPTIONS);
4695
4874
  },
4696
4875
  getPredictionSummary() {
@@ -4801,5 +4980,5 @@ function ldsEngineCreator() {
4801
4980
  return { name: 'ldsEngineCreator' };
4802
4981
  }
4803
4982
 
4804
- export { buildPredictorForContext, ldsEngineCreator as default, initializeLDS, initializeOneStore };
4805
- // version: 1.314.0-5a451c78cd
4983
+ export { LexRequestStrategy, buildPredictorForContext, ldsEngineCreator as default, initializeLDS, initializeOneStore, registerRequestStrategy, saveRequestAsPrediction, unregisterRequestStrategy, whenPredictionsReady };
4984
+ // version: 1.316.0-fd56ed976d