@salesforce/lds-runtime-aura 1.309.0-dev13 → 1.309.0-dev15

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