@salesforce/lds-runtime-aura 1.313.0 → 1.315.0

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