@salesforce/lds-runtime-aura 1.266.0-dev20 → 1.266.0-dev22
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.
- package/dist/ldsEngineCreator.js +1034 -1027
- package/dist/types/__mocks__/@salesforce/lds-instrumentation.d.ts +5 -0
- package/dist/types/aura-instrumentation/main.d.ts +2 -2
- package/dist/types/main.d.ts +1 -1
- package/dist/types/predictive-loading/common/index.d.ts +3 -0
- package/dist/types/predictive-loading/index.d.ts +1 -0
- package/package.json +11 -11
package/dist/ldsEngineCreator.js
CHANGED
|
@@ -15,16 +15,20 @@
|
|
|
15
15
|
import { HttpStatusCode, InMemoryStore, Environment, Luvio, InMemoryStoreQueryEvaluator } from 'force/luvioEngine';
|
|
16
16
|
import ldsTrackedFieldsBehaviorGate from '@salesforce/gate/lds.useNewTrackedFieldBehavior';
|
|
17
17
|
import usePredictiveLoading from '@salesforce/gate/lds.usePredictiveLoading';
|
|
18
|
-
import { getRecordAvatarsAdapterFactory, getRecordAdapterFactory, coerceFieldIdArray, getRecordsAdapterFactory, getRecordActionsAdapterFactory, getObjectInfoAdapterFactory, getObjectInfosAdapterFactory,
|
|
19
|
-
import {
|
|
20
|
-
import { withRegistration, register, setDefaultLuvio } from 'force/ldsEngine';
|
|
18
|
+
import { instrument, getRecordAvatarsAdapterFactory, getRecordAdapterFactory, coerceFieldIdArray, getRecordsAdapterFactory, getRecordActionsAdapterFactory, getObjectInfoAdapterFactory, getObjectInfosAdapterFactory, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, registerPrefetcher } from 'force/ldsAdaptersUiapi';
|
|
19
|
+
import { LRUCache, instrumentAdapter, instrumentLuvio, setupInstrumentation as setupInstrumentation$1, logObjectInfoChanged as logObjectInfoChanged$1, updatePercentileHistogramMetric, incrementCounterMetric, incrementGetRecordNotifyChangeAllowCount, incrementGetRecordNotifyChangeDropCount, incrementNotifyRecordUpdateAvailableAllowCount, incrementNotifyRecordUpdateAvailableDropCount, setLdsAdaptersUiapiInstrumentation, setLdsNetworkAdapterInstrumentation, executeAsyncActivity, METRIC_KEYS, onIdleDetected } from 'force/ldsInstrumentation';
|
|
21
20
|
import { REFRESH_ADAPTER_EVENT, ADAPTER_UNFULFILLED_ERROR, instrument as instrument$2 } from 'force/ldsBindings';
|
|
22
21
|
import { counter, registerCacheStats, perfStart, perfEnd, registerPeriodicLogger, interaction, timer } from 'instrumentation/service';
|
|
23
|
-
import { LRUCache, instrumentAdapter, instrumentLuvio, setupInstrumentation as setupInstrumentation$1, logObjectInfoChanged as logObjectInfoChanged$1, updatePercentileHistogramMetric, incrementCounterMetric, incrementGetRecordNotifyChangeAllowCount, incrementGetRecordNotifyChangeDropCount, incrementNotifyRecordUpdateAvailableAllowCount, incrementNotifyRecordUpdateAvailableDropCount, setLdsAdaptersUiapiInstrumentation, setLdsNetworkAdapterInstrumentation, onIdleDetected } from 'force/ldsInstrumentation';
|
|
24
22
|
import auraNetworkAdapter, { instrument as instrument$1, forceRecordTransactionsDisabled, ldsNetworkAdapterInstrument, dispatchAuraAction, defaultActionConfig } from 'force/ldsNetwork';
|
|
25
23
|
import { instrument as instrument$3 } from 'force/adsBridge';
|
|
24
|
+
import { withRegistration, register, setDefaultLuvio } from 'force/ldsEngine';
|
|
25
|
+
import { createStorage, clearStorages } from 'force/ldsStorage';
|
|
26
26
|
import { buildJwtNetworkAdapter } from 'force/ldsNetworkFetchWithJwt';
|
|
27
27
|
|
|
28
|
+
const PDL_EXECUTE_ASYNC_OPTIONS = {
|
|
29
|
+
LOG_ERROR_ONLY: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
28
32
|
class PredictivePrefetchPage {
|
|
29
33
|
constructor(context) {
|
|
30
34
|
this.context = context;
|
|
@@ -167,94 +171,56 @@ class RecordHomePage extends PredictivePrefetchPage {
|
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.queuedPredictionRequests.push(...predictedRequests);
|
|
216
|
-
return Promise.all(predictedRequests.map((request) => this.requestRunner.runRequest(request))).then();
|
|
217
|
-
}
|
|
218
|
-
hasPredictions() {
|
|
219
|
-
const exactPageRequests = this.repository.getPageRequests(this.context) || [];
|
|
220
|
-
const similarPageRequests = this.page.similarContext !== undefined
|
|
221
|
-
? this.repository.getPageRequests(this.page.similarContext)
|
|
222
|
-
: [];
|
|
223
|
-
return exactPageRequests.length > 0 || similarPageRequests.length > 0;
|
|
224
|
-
}
|
|
225
|
-
getSimilarPageRequests() {
|
|
226
|
-
let resolvedSimilarPageRequests = [];
|
|
227
|
-
if (this.page.similarContext !== undefined) {
|
|
228
|
-
const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
|
|
229
|
-
if (similarPageRequests !== undefined) {
|
|
230
|
-
resolvedSimilarPageRequests = similarPageRequests.map((request) => this.page.resolveSimilarRequest(request));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return resolvedSimilarPageRequests;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
174
|
+
/**
|
|
175
|
+
* Observability / Critical Availability Program (230+)
|
|
176
|
+
*
|
|
177
|
+
* This file is intended to be used as a consolidated place for all definitions, functions,
|
|
178
|
+
* and helpers related to "M1"[1].
|
|
179
|
+
*
|
|
180
|
+
* Below are the R.E.A.D.S. metrics for the Lightning Data Service, defined here[2].
|
|
181
|
+
*
|
|
182
|
+
* [1] Search "[M1] Lightning Data Service Design Spike" in Quip
|
|
183
|
+
* [2] Search "Lightning Data Service R.E.A.D.S. Metrics" in Quip
|
|
184
|
+
*/
|
|
185
|
+
const OBSERVABILITY_NAMESPACE = 'LIGHTNING.lds.service';
|
|
186
|
+
const ADAPTER_INVOCATION_COUNT_METRIC_NAME = 'request';
|
|
187
|
+
const ADAPTER_ERROR_COUNT_METRIC_NAME = 'error';
|
|
188
|
+
const NETWORK_ADAPTER_RESPONSE_METRIC_NAME = 'network-response';
|
|
189
|
+
/**
|
|
190
|
+
* W-8379680
|
|
191
|
+
* Counter for number of getApex requests.
|
|
192
|
+
*/
|
|
193
|
+
const GET_APEX_REQUEST_COUNT = {
|
|
194
|
+
get() {
|
|
195
|
+
return {
|
|
196
|
+
owner: OBSERVABILITY_NAMESPACE,
|
|
197
|
+
name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* W-8828410
|
|
203
|
+
* Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
|
|
204
|
+
*/
|
|
205
|
+
const TOTAL_ADAPTER_ERROR_COUNT = {
|
|
206
|
+
get() {
|
|
207
|
+
return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* W-8828410
|
|
212
|
+
* Counter for the number of invocations made into LDS by a wire adapter.
|
|
213
|
+
*/
|
|
214
|
+
const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
|
|
215
|
+
get() {
|
|
216
|
+
return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
|
|
217
|
+
},
|
|
218
|
+
};
|
|
236
219
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// from scripts/lds-uiapi-plugin.js
|
|
241
|
-
requestStrategies) {
|
|
242
|
-
super(context, repository, requestRunner);
|
|
243
|
-
this.requestStrategies = requestStrategies;
|
|
244
|
-
this.page = this.getPage();
|
|
245
|
-
}
|
|
246
|
-
getPage() {
|
|
247
|
-
if (RecordHomePage.handlesContext(this.context)) {
|
|
248
|
-
return new RecordHomePage(this.context, this.requestStrategies);
|
|
249
|
-
}
|
|
250
|
-
return new LexDefaultPage(this.context);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
220
|
+
const { create, keys } = Object;
|
|
221
|
+
const { isArray } = Array;
|
|
222
|
+
const { stringify } = JSON;
|
|
253
223
|
|
|
254
|
-
// Copy-pasted from adapter-utils. This util should be extracted from generated code and imported in prefetch repository.
|
|
255
|
-
const { keys: ObjectKeys$1 } = Object;
|
|
256
|
-
const { stringify: JSONStringify } = JSON;
|
|
257
|
-
const { isArray: ArrayIsArray } = Array;
|
|
258
224
|
/**
|
|
259
225
|
* A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
|
|
260
226
|
* This is needed because insertion order for JSON.stringify(object) affects output:
|
|
@@ -262,6 +228,7 @@ const { isArray: ArrayIsArray } = Array;
|
|
|
262
228
|
* "{"a":1,"b":2}"
|
|
263
229
|
* JSON.stringify({b: 2, a: 1})
|
|
264
230
|
* "{"b":2,"a":1}"
|
|
231
|
+
* Modified from the apex implementation to sort arrays non-destructively.
|
|
265
232
|
* @param data Data to be JSON-stringified.
|
|
266
233
|
* @returns JSON.stringified value with consistent ordering of keys.
|
|
267
234
|
*/
|
|
@@ -278,11 +245,14 @@ function stableJSONStringify$1(node) {
|
|
|
278
245
|
return isFinite(node) ? '' + node : 'null';
|
|
279
246
|
}
|
|
280
247
|
if (typeof node !== 'object') {
|
|
281
|
-
return
|
|
248
|
+
return stringify(node);
|
|
282
249
|
}
|
|
283
250
|
let i;
|
|
284
251
|
let out;
|
|
285
|
-
if (
|
|
252
|
+
if (isArray(node)) {
|
|
253
|
+
// copy any array before sorting so we don't mutate the object.
|
|
254
|
+
// eslint-disable-next-line no-param-reassign
|
|
255
|
+
node = node.slice(0).sort();
|
|
286
256
|
out = '[';
|
|
287
257
|
for (i = 0; i < node.length; i++) {
|
|
288
258
|
if (i) {
|
|
@@ -295,10 +265,10 @@ function stableJSONStringify$1(node) {
|
|
|
295
265
|
if (node === null) {
|
|
296
266
|
return 'null';
|
|
297
267
|
}
|
|
298
|
-
const keys =
|
|
268
|
+
const keys$1 = keys(node).sort();
|
|
299
269
|
out = '';
|
|
300
|
-
for (i = 0; i < keys.length; i++) {
|
|
301
|
-
const key = keys[i];
|
|
270
|
+
for (i = 0; i < keys$1.length; i++) {
|
|
271
|
+
const key = keys$1[i];
|
|
302
272
|
const value = stableJSONStringify$1(node[key]);
|
|
303
273
|
if (!value) {
|
|
304
274
|
continue;
|
|
@@ -306,532 +276,565 @@ function stableJSONStringify$1(node) {
|
|
|
306
276
|
if (out) {
|
|
307
277
|
out += ',';
|
|
308
278
|
}
|
|
309
|
-
out +=
|
|
279
|
+
out += stringify(key) + ':' + value;
|
|
310
280
|
}
|
|
311
281
|
return '{' + out + '}';
|
|
312
282
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
function deepEquals(objA, objB) {
|
|
317
|
-
if (objA === objB)
|
|
318
|
-
return true;
|
|
319
|
-
if (objA instanceof Date && objB instanceof Date)
|
|
320
|
-
return objA.getTime() === objB.getTime();
|
|
321
|
-
// If one of them is not an object, they are not deeply equal
|
|
322
|
-
if (!isObject(objA) || !isObject(objB))
|
|
323
|
-
return false;
|
|
324
|
-
// Filter out keys set as undefined, we can compare undefined as equals.
|
|
325
|
-
const keysA = ObjectKeys$1(objA).filter((key) => objA[key] !== undefined);
|
|
326
|
-
const keysB = ObjectKeys$1(objB).filter((key) => objB[key] !== undefined);
|
|
327
|
-
// If the objects do not have the same set of keys, they are not deeply equal
|
|
328
|
-
if (keysA.length !== keysB.length)
|
|
329
|
-
return false;
|
|
330
|
-
for (const key of keysA) {
|
|
331
|
-
const valA = objA[key];
|
|
332
|
-
const valB = objB[key];
|
|
333
|
-
const areObjects = isObject(valA) && isObject(valB);
|
|
334
|
-
// If both values are objects, recursively compare them
|
|
335
|
-
if (areObjects && !deepEquals(valA, valB))
|
|
336
|
-
return false;
|
|
337
|
-
// If only one value is an object or if the values are not strictly equal, they are not deeply equal
|
|
338
|
-
if (!areObjects && valA !== valB)
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
return true;
|
|
283
|
+
function isPromise(value) {
|
|
284
|
+
// check for Thenable due to test frameworks using custom Promise impls
|
|
285
|
+
return value !== null && value.then !== undefined;
|
|
342
286
|
}
|
|
343
287
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
288
|
+
const APEX_ADAPTER_NAME = 'getApex';
|
|
289
|
+
const NORMALIZED_APEX_ADAPTER_NAME = `Apex.${APEX_ADAPTER_NAME}`;
|
|
290
|
+
const REFRESH_APEX_KEY = 'refreshApex';
|
|
291
|
+
const REFRESH_UIAPI_KEY = 'refreshUiApi';
|
|
292
|
+
const SUPPORTED_KEY = 'refreshSupported';
|
|
293
|
+
const UNSUPPORTED_KEY = 'refreshUnsupported';
|
|
294
|
+
const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
|
|
295
|
+
const REFRESH_EVENTTYPE = 'system';
|
|
296
|
+
const REFRESH_PAYLOAD_TARGET = 'adapters';
|
|
297
|
+
const REFRESH_PAYLOAD_SCOPE = 'lds';
|
|
298
|
+
const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
|
|
299
|
+
const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
|
|
300
|
+
const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
|
|
301
|
+
const NAMESPACE = 'lds';
|
|
302
|
+
const NETWORK_TRANSACTION_NAME = 'lds-network';
|
|
303
|
+
const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
|
|
304
|
+
// Aggregate Cache Stats and Metrics for all getApex invocations
|
|
305
|
+
const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
|
|
306
|
+
const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
|
|
307
|
+
// Observability (READS)
|
|
308
|
+
const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
|
|
309
|
+
const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
|
|
310
|
+
const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
|
|
311
|
+
class Instrumentation {
|
|
312
|
+
constructor() {
|
|
313
|
+
this.adapterUnfulfilledErrorCounters = {};
|
|
314
|
+
this.recordApiNameChangeCounters = {};
|
|
315
|
+
this.refreshAdapterEvents = {};
|
|
316
|
+
this.refreshApiCallEventStats = {
|
|
317
|
+
[REFRESH_APEX_KEY]: 0,
|
|
318
|
+
[REFRESH_UIAPI_KEY]: 0,
|
|
319
|
+
[SUPPORTED_KEY]: 0,
|
|
320
|
+
[UNSUPPORTED_KEY]: 0,
|
|
321
|
+
};
|
|
322
|
+
this.lastRefreshApiCall = null;
|
|
323
|
+
this.weakEtagZeroEvents = {};
|
|
324
|
+
this.adapterCacheMisses = new LRUCache(250);
|
|
325
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
326
|
+
window.addEventListener('beforeunload', () => {
|
|
327
|
+
if (keys(this.weakEtagZeroEvents).length > 0) {
|
|
328
|
+
perfStart(NETWORK_TRANSACTION_NAME);
|
|
329
|
+
perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
|
|
361
330
|
}
|
|
362
|
-
existingRequestEntry.requestMetadata.requestTimes.push(requestTime);
|
|
363
331
|
});
|
|
364
|
-
await this.storage.set(id, page);
|
|
365
332
|
}
|
|
366
|
-
this.
|
|
367
|
-
}
|
|
368
|
-
async saveRequest(key, request) {
|
|
369
|
-
const identifier = stableJSONStringify$1(key);
|
|
370
|
-
const batchForKey = this.requestBuffer.get(identifier) || [];
|
|
371
|
-
batchForKey.push({
|
|
372
|
-
request,
|
|
373
|
-
requestTime: Date.now(),
|
|
374
|
-
});
|
|
375
|
-
this.requestBuffer.set(identifier, batchForKey);
|
|
333
|
+
registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
|
|
376
334
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Instruments an existing adapter to log argus metrics and cache stats.
|
|
337
|
+
* @param adapter The adapter function.
|
|
338
|
+
* @param metadata The adapter metadata.
|
|
339
|
+
* @param wireConfigKeyFn Optional function to transform wire configs to a unique key.
|
|
340
|
+
* @returns The wrapped adapter.
|
|
341
|
+
*/
|
|
342
|
+
instrumentAdapter(adapter, metadata) {
|
|
343
|
+
// We are consolidating all apex adapter instrumentation calls under a single key
|
|
344
|
+
const { apiFamily, name, ttl } = metadata;
|
|
345
|
+
const adapterName = normalizeAdapterName(name, apiFamily);
|
|
346
|
+
const isGetApexAdapter = isApexAdapter(name);
|
|
347
|
+
const stats = isGetApexAdapter ? getApexCacheStats : registerLdsCacheStats(adapterName);
|
|
348
|
+
const ttlMissStats = isGetApexAdapter
|
|
349
|
+
? getApexTtlCacheStats
|
|
350
|
+
: registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
|
|
351
|
+
/**
|
|
352
|
+
* W-8076905
|
|
353
|
+
* Dynamically generated metric. Simple counter for all requests made by this adapter.
|
|
354
|
+
*/
|
|
355
|
+
const wireAdapterRequestMetric = isGetApexAdapter
|
|
356
|
+
? getApexRequestCountMetric
|
|
357
|
+
: counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
|
|
358
|
+
const instrumentedAdapter = (config, requestContext) => {
|
|
359
|
+
// increment overall and adapter request metrics
|
|
360
|
+
wireAdapterRequestMetric.increment(1);
|
|
361
|
+
totalAdapterRequestSuccessMetric.increment(1);
|
|
362
|
+
// execute adapter logic
|
|
363
|
+
const result = adapter(config, requestContext);
|
|
364
|
+
// In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
|
|
365
|
+
// (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
|
|
366
|
+
//
|
|
367
|
+
// Note: we can't do a plain instanceof check for a promise here since the Promise may
|
|
368
|
+
// originate from another javascript realm (for example: in jest test). Instead we use a
|
|
369
|
+
// duck-typing approach by checking if the result has a then property.
|
|
370
|
+
//
|
|
371
|
+
// For adapters without persistent store:
|
|
372
|
+
// - total cache hit ratio:
|
|
373
|
+
// [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
|
|
374
|
+
// For adapters with persistent store:
|
|
375
|
+
// - in-memory cache hit ratio:
|
|
376
|
+
// [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
|
|
377
|
+
// - total cache hit ratio:
|
|
378
|
+
// ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
|
|
379
|
+
// if result === null then config is insufficient/invalid so do not log
|
|
380
|
+
if (isPromise(result)) {
|
|
381
|
+
stats.logMisses();
|
|
382
|
+
if (ttl !== undefined) {
|
|
383
|
+
this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
|
|
421
384
|
}
|
|
422
|
-
reducedRequests.push(combinedRequest);
|
|
423
|
-
visitedRequests.add(currentRequest);
|
|
424
385
|
}
|
|
425
|
-
|
|
426
|
-
|
|
386
|
+
else if (result !== null) {
|
|
387
|
+
stats.logHits();
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
};
|
|
391
|
+
// Set the name property on the function for debugging purposes.
|
|
392
|
+
Object.defineProperty(instrumentedAdapter, 'name', {
|
|
393
|
+
value: name + '__instrumented',
|
|
394
|
+
});
|
|
395
|
+
return instrumentAdapter(instrumentedAdapter, metadata);
|
|
427
396
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
397
|
+
/**
|
|
398
|
+
* 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.
|
|
399
|
+
* Backed by an LRU Cache implementation to prevent too many record entries from being stored in-memory.
|
|
400
|
+
* @param name The wire adapter name.
|
|
401
|
+
* @param config The config passed into wire adapter.
|
|
402
|
+
* @param ttlMissStats CacheStatsLogger to log misses out of TTL.
|
|
403
|
+
* @param currentCacheMissTimestamp Timestamp for when the request was made.
|
|
404
|
+
* @param ttl TTL for the wire adapter.
|
|
405
|
+
*/
|
|
406
|
+
logAdapterCacheMissOutOfTtlDuration(name, config, ttlMissStats, currentCacheMissTimestamp, ttl) {
|
|
407
|
+
const configKey = `${name}:${stableJSONStringify$1(config)}`;
|
|
408
|
+
const existingCacheMissTimestamp = this.adapterCacheMisses.get(configKey);
|
|
409
|
+
this.adapterCacheMisses.set(configKey, currentCacheMissTimestamp);
|
|
410
|
+
if (existingCacheMissTimestamp !== undefined) {
|
|
411
|
+
const duration = currentCacheMissTimestamp - existingCacheMissTimestamp;
|
|
412
|
+
if (duration > ttl) {
|
|
413
|
+
ttlMissStats.logMisses();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
431
416
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
417
|
+
/**
|
|
418
|
+
* Injected to LDS for Luvio specific instrumentation.
|
|
419
|
+
*
|
|
420
|
+
* @param context The transaction context.
|
|
421
|
+
*/
|
|
422
|
+
instrumentLuvio(context) {
|
|
423
|
+
instrumentLuvio(context);
|
|
424
|
+
if (this.isRefreshAdapterEvent(context)) {
|
|
425
|
+
this.aggregateRefreshAdapterEvents(context);
|
|
436
426
|
}
|
|
437
|
-
|
|
427
|
+
else if (this.isAdapterUnfulfilledError(context)) {
|
|
428
|
+
this.incrementAdapterRequestErrorCount(context);
|
|
429
|
+
}
|
|
430
|
+
else ;
|
|
438
431
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Returns whether or not this is a RefreshAdapterEvent.
|
|
434
|
+
* @param context The transaction context.
|
|
435
|
+
* @returns Whether or not this is a RefreshAdapterEvent.
|
|
436
|
+
*/
|
|
437
|
+
isRefreshAdapterEvent(context) {
|
|
438
|
+
return context[REFRESH_ADAPTER_EVENT] === true;
|
|
444
439
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Returns whether or not this is an AdapterUnfulfilledError.
|
|
442
|
+
* @param context The transaction context.
|
|
443
|
+
* @returns Whether or not this is an AdapterUnfulfilledError.
|
|
444
|
+
*/
|
|
445
|
+
isAdapterUnfulfilledError(context) {
|
|
446
|
+
return context[ADAPTER_UNFULFILLED_ERROR] === true;
|
|
452
447
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Specific instrumentation for getRecordNotifyChange.
|
|
450
|
+
* temporary implementation to match existing aura call for now
|
|
451
|
+
*
|
|
452
|
+
* @param uniqueWeakEtags whether weakEtags match or not
|
|
453
|
+
* @param error if dispatchResourceRequest fails for any reason
|
|
454
|
+
*/
|
|
455
|
+
notifyChangeNetwork(uniqueWeakEtags, error) {
|
|
456
|
+
perfStart(NETWORK_TRANSACTION_NAME);
|
|
457
|
+
if (error === true) {
|
|
458
|
+
perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
|
|
462
|
+
}
|
|
461
463
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
context: similarContext,
|
|
464
|
+
/**
|
|
465
|
+
* Parses and aggregates weakETagZero events to be sent in summarized log line.
|
|
466
|
+
* @param context The transaction context.
|
|
467
|
+
*/
|
|
468
|
+
aggregateWeakETagEvents(incomingWeakEtagZero, existingWeakEtagZero, apiName) {
|
|
469
|
+
const key = 'weaketag-0-' + apiName;
|
|
470
|
+
if (this.weakEtagZeroEvents[key] === undefined) {
|
|
471
|
+
this.weakEtagZeroEvents[key] = {
|
|
472
|
+
[EXISTING_WEAKETAG_0_KEY]: 0,
|
|
473
|
+
[INCOMING_WEAKETAG_0_KEY]: 0,
|
|
473
474
|
};
|
|
474
475
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return (request.config.recordIds &&
|
|
482
|
-
(context.recordId === request.config.recordIds || // some may set this as string instead of array
|
|
483
|
-
(request.config.recordIds.length === 1 &&
|
|
484
|
-
request.config.recordIds[0] === context.recordId)));
|
|
476
|
+
if (existingWeakEtagZero) {
|
|
477
|
+
this.weakEtagZeroEvents[key][EXISTING_WEAKETAG_0_KEY] += 1;
|
|
478
|
+
}
|
|
479
|
+
if (incomingWeakEtagZero) {
|
|
480
|
+
this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
|
|
481
|
+
}
|
|
485
482
|
}
|
|
486
|
-
|
|
487
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Aggregates refresh adapter events to be sent in summarized log line.
|
|
485
|
+
* - how many times refreshApex is called
|
|
486
|
+
* - how many times refresh from lightning/uiRecordApi is called
|
|
487
|
+
* - number of supported calls: refreshApex called on apex adapter
|
|
488
|
+
* - number of unsupported calls: refreshApex on non-apex adapter
|
|
489
|
+
* + any use of refresh from uiRecordApi module
|
|
490
|
+
* - count of refresh calls per adapter
|
|
491
|
+
* @param context The refresh adapter event.
|
|
492
|
+
*/
|
|
493
|
+
aggregateRefreshAdapterEvents(context) {
|
|
494
|
+
// We are consolidating all apex adapter instrumentation calls under a single key
|
|
495
|
+
// Adding additional logging that getApex adapters can invoke? Read normalizeAdapterName ts-doc.
|
|
496
|
+
const adapterName = normalizeAdapterName(context.adapterName);
|
|
497
|
+
if (this.lastRefreshApiCall === REFRESH_APEX_KEY) {
|
|
498
|
+
if (isApexAdapter(adapterName)) {
|
|
499
|
+
this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
|
|
506
|
+
this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
|
|
507
|
+
}
|
|
508
|
+
if (this.refreshAdapterEvents[adapterName] === undefined) {
|
|
509
|
+
this.refreshAdapterEvents[adapterName] = 0;
|
|
510
|
+
}
|
|
511
|
+
this.refreshAdapterEvents[adapterName] += 1;
|
|
512
|
+
this.lastRefreshApiCall = null;
|
|
488
513
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
514
|
+
/**
|
|
515
|
+
* Increments call stat for incoming refresh api call, and sets the name
|
|
516
|
+
* to be used in {@link aggregateRefreshCalls}
|
|
517
|
+
* @param from The name of the refresh function called.
|
|
518
|
+
*/
|
|
519
|
+
handleRefreshApiCall(apiName) {
|
|
520
|
+
this.refreshApiCallEventStats[apiName] += 1;
|
|
521
|
+
// set function call to be used with aggregateRefreshCalls
|
|
522
|
+
this.lastRefreshApiCall = apiName;
|
|
493
523
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
this.
|
|
500
|
-
|
|
524
|
+
/**
|
|
525
|
+
* W-7302241
|
|
526
|
+
* Logs refresh call summary stats as a LightningInteraction.
|
|
527
|
+
*/
|
|
528
|
+
logRefreshStats() {
|
|
529
|
+
if (keys(this.refreshAdapterEvents).length > 0) {
|
|
530
|
+
interaction(REFRESH_PAYLOAD_TARGET, REFRESH_PAYLOAD_SCOPE, this.refreshAdapterEvents, REFRESH_EVENTSOURCE, REFRESH_EVENTTYPE, this.refreshApiCallEventStats);
|
|
531
|
+
this.resetRefreshStats();
|
|
532
|
+
}
|
|
501
533
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Resets the stat trackers for refresh call events.
|
|
536
|
+
*/
|
|
537
|
+
resetRefreshStats() {
|
|
538
|
+
this.refreshAdapterEvents = {};
|
|
539
|
+
this.refreshApiCallEventStats = {
|
|
540
|
+
[REFRESH_APEX_KEY]: 0,
|
|
541
|
+
[REFRESH_UIAPI_KEY]: 0,
|
|
542
|
+
[SUPPORTED_KEY]: 0,
|
|
543
|
+
[UNSUPPORTED_KEY]: 0,
|
|
509
544
|
};
|
|
545
|
+
this.lastRefreshApiCall = null;
|
|
510
546
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
547
|
+
/**
|
|
548
|
+
* W-7801618
|
|
549
|
+
* Counter for occurrences where the incoming record to be merged has a different apiName.
|
|
550
|
+
* Dynamically generated metric, stored in an {@link RecordApiNameChangeCounters} object.
|
|
551
|
+
*
|
|
552
|
+
* @param context The transaction context.
|
|
553
|
+
*
|
|
554
|
+
* Note: Short-lived metric candidate, remove at the end of 230
|
|
555
|
+
*/
|
|
556
|
+
incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
|
|
557
|
+
let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
|
|
558
|
+
if (apiNameChangeCounter === undefined) {
|
|
559
|
+
apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
|
|
560
|
+
this.recordApiNameChangeCounters[existingApiName] = apiNameChangeCounter;
|
|
514
561
|
}
|
|
515
|
-
|
|
516
|
-
let optionalFields = coerceFieldIdArray(request.config.optionalFields) || [];
|
|
517
|
-
return {
|
|
518
|
-
...request,
|
|
519
|
-
config: {
|
|
520
|
-
...request.config,
|
|
521
|
-
fields: undefined,
|
|
522
|
-
optionalFields: [...fields, ...optionalFields],
|
|
523
|
-
},
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
canCombine(reqA, reqB) {
|
|
527
|
-
// must be same record and
|
|
528
|
-
return (reqA.recordId === reqB.recordId &&
|
|
529
|
-
// both requests are fields requests
|
|
530
|
-
(reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
|
|
531
|
-
(reqB.fields !== undefined || reqB.optionalFields !== undefined));
|
|
562
|
+
apiNameChangeCounter.increment(1);
|
|
532
563
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
reqB.optionalFields.forEach((field) => optionalFields.add(field));
|
|
564
|
+
/**
|
|
565
|
+
* W-8620679
|
|
566
|
+
* Increment the counter for an UnfulfilledSnapshotError coming from luvio
|
|
567
|
+
*
|
|
568
|
+
* @param context The transaction context.
|
|
569
|
+
*/
|
|
570
|
+
incrementAdapterRequestErrorCount(context) {
|
|
571
|
+
// We are consolidating all apex adapter instrumentation calls under a single key
|
|
572
|
+
const adapterName = normalizeAdapterName(context.adapterName);
|
|
573
|
+
let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
|
|
574
|
+
if (adapterRequestErrorCounter === undefined) {
|
|
575
|
+
adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
|
|
576
|
+
this.adapterUnfulfilledErrorCounters[adapterName] = adapterRequestErrorCounter;
|
|
547
577
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
fields: Array.from(fields),
|
|
551
|
-
optionalFields: Array.from(optionalFields),
|
|
552
|
-
};
|
|
578
|
+
adapterRequestErrorCounter.increment(1);
|
|
579
|
+
totalAdapterErrorMetric.increment(1);
|
|
553
580
|
}
|
|
554
581
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
this.adapterName = 'getRecords';
|
|
560
|
-
this.adapterFactory = getRecordsAdapterFactory;
|
|
561
|
-
}
|
|
562
|
-
buildConcreteRequest(similarRequest, context) {
|
|
563
|
-
return {
|
|
564
|
-
...similarRequest,
|
|
565
|
-
config: {
|
|
566
|
-
...similarRequest.config,
|
|
567
|
-
records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
|
|
568
|
-
},
|
|
569
|
-
};
|
|
582
|
+
function createMetricsKey(owner, name, unit) {
|
|
583
|
+
let metricName = name;
|
|
584
|
+
if (unit) {
|
|
585
|
+
metricName = metricName + '.' + unit;
|
|
570
586
|
}
|
|
587
|
+
return {
|
|
588
|
+
get() {
|
|
589
|
+
return { owner: owner, name: metricName };
|
|
590
|
+
},
|
|
591
|
+
};
|
|
571
592
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return
|
|
593
|
+
/**
|
|
594
|
+
* Returns whether adapter is an Apex one or not.
|
|
595
|
+
* @param adapterName The name of the adapter.
|
|
596
|
+
*/
|
|
597
|
+
function isApexAdapter(adapterName) {
|
|
598
|
+
return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
|
|
578
599
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
600
|
+
/**
|
|
601
|
+
* Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
|
|
602
|
+
* API family, if supplied. Example: `UiApi.getRecord`.
|
|
603
|
+
*
|
|
604
|
+
* Note: If you are adding additional logging that can come from getApex adapter contexts that provide
|
|
605
|
+
* the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
|
|
606
|
+
* ensure to call this method to normalize all logging to 'getApex'. This
|
|
607
|
+
* is because Argus has a 50k key cardinality limit. More context: W-8379680.
|
|
608
|
+
*
|
|
609
|
+
* @param adapterName The name of the adapter.
|
|
610
|
+
* @param apiFamily The API family of the adapter.
|
|
611
|
+
*/
|
|
612
|
+
function normalizeAdapterName(adapterName, apiFamily) {
|
|
613
|
+
if (isApexAdapter(adapterName)) {
|
|
614
|
+
return NORMALIZED_APEX_ADAPTER_NAME;
|
|
582
615
|
}
|
|
583
|
-
return
|
|
616
|
+
return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
|
|
584
617
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
recordIds: [context.recordId],
|
|
597
|
-
},
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
buildGetRecordActionsSaveRequestData(similarContext, context, request) {
|
|
601
|
-
if (this.isGetRecordActionsRequestContextDependent(context, request)) {
|
|
602
|
-
return {
|
|
603
|
-
request: this.transformForSave({
|
|
604
|
-
...request,
|
|
605
|
-
config: {
|
|
606
|
-
...request.config,
|
|
607
|
-
recordIds: ['*'],
|
|
608
|
-
},
|
|
609
|
-
}),
|
|
610
|
-
context: similarContext,
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
return {
|
|
614
|
-
request: this.transformForSave(request),
|
|
615
|
-
context,
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
canCombine(reqA, reqB) {
|
|
619
|
-
return (reqA.retrievalMode === reqB.retrievalMode &&
|
|
620
|
-
reqA.formFactor === reqB.formFactor &&
|
|
621
|
-
(reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
|
|
622
|
-
(reqA.sections || []).toString() === (reqB.sections || []).toString());
|
|
623
|
-
}
|
|
624
|
-
combineRequests(reqA, reqB) {
|
|
625
|
-
const combined = { ...reqA };
|
|
626
|
-
// let's merge the recordIds
|
|
627
|
-
combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
|
|
628
|
-
if (combined.retrievalMode === 'ALL') {
|
|
629
|
-
const combinedSet = new Set([
|
|
630
|
-
...normalizeApiNames(combined.apiNames),
|
|
631
|
-
...normalizeApiNames(reqB.apiNames),
|
|
632
|
-
]);
|
|
633
|
-
combined.apiNames = Array.from(combinedSet);
|
|
634
|
-
}
|
|
635
|
-
return combined;
|
|
618
|
+
const timerMetricTracker = create(null);
|
|
619
|
+
/**
|
|
620
|
+
* Calls instrumentation/service telemetry timer
|
|
621
|
+
* @param name Name of the metric
|
|
622
|
+
* @param duration number to update backing percentile histogram, negative numbers ignored
|
|
623
|
+
*/
|
|
624
|
+
function updateTimerMetric(name, duration) {
|
|
625
|
+
let metric = timerMetricTracker[name];
|
|
626
|
+
if (metric === undefined) {
|
|
627
|
+
metric = timer(createMetricsKey(NAMESPACE, name));
|
|
628
|
+
timerMetricTracker[name] = metric;
|
|
636
629
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
630
|
+
timerMetricAddDuration(metric, duration);
|
|
631
|
+
}
|
|
632
|
+
function timerMetricAddDuration(timer, duration) {
|
|
633
|
+
// Guard against negative values since it causes error to be thrown by MetricsService
|
|
634
|
+
if (duration >= 0) {
|
|
635
|
+
timer.addDuration(duration);
|
|
642
636
|
}
|
|
643
637
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
638
|
+
/**
|
|
639
|
+
* W-10315098
|
|
640
|
+
* Increments the counter associated with the request response. Counts are bucketed by status.
|
|
641
|
+
*/
|
|
642
|
+
const requestResponseMetricTracker = create(null);
|
|
643
|
+
function incrementRequestResponseCount(cb) {
|
|
644
|
+
const status = cb().status;
|
|
645
|
+
let metric = requestResponseMetricTracker[status];
|
|
646
|
+
if (metric === undefined) {
|
|
647
|
+
metric = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, NETWORK_ADAPTER_RESPONSE_METRIC_NAME, `${status.valueOf()}`));
|
|
648
|
+
requestResponseMetricTracker[status] = metric;
|
|
650
649
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
650
|
+
metric.increment();
|
|
651
|
+
}
|
|
652
|
+
function logObjectInfoChanged() {
|
|
653
|
+
logObjectInfoChanged$1();
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Create a new instrumentation cache stats and return it.
|
|
657
|
+
*
|
|
658
|
+
* @param name The cache logger name.
|
|
659
|
+
*/
|
|
660
|
+
function registerLdsCacheStats(name) {
|
|
661
|
+
return registerCacheStats(`${NAMESPACE}:${name}`);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Add or overwrite hooks that require aura implementations
|
|
665
|
+
*/
|
|
666
|
+
function setAuraInstrumentationHooks() {
|
|
667
|
+
instrument({
|
|
668
|
+
recordConflictsResolved: (serverRequestCount) => {
|
|
669
|
+
// Ignore 0 values which can originate from ADS bridge
|
|
670
|
+
if (serverRequestCount > 0) {
|
|
671
|
+
updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
|
|
675
|
+
const metricName = `merge-null-dv-count.${fieldType}`;
|
|
676
|
+
if (fieldType === 'scalar') {
|
|
677
|
+
incrementCounterMetric(`${metricName}.${areValuesEqual}`);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
incrementCounterMetric(metricName);
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
|
|
684
|
+
getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
|
|
685
|
+
notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
|
|
686
|
+
notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
|
|
687
|
+
recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
|
|
688
|
+
weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
|
|
689
|
+
getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
|
|
690
|
+
});
|
|
691
|
+
withRegistration('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
|
|
692
|
+
instrument$1({
|
|
693
|
+
logCrud: logCRUDLightningInteraction,
|
|
694
|
+
networkResponse: incrementRequestResponseCount,
|
|
695
|
+
});
|
|
696
|
+
instrument$2({
|
|
697
|
+
refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
|
|
698
|
+
instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
|
|
699
|
+
});
|
|
700
|
+
instrument$3({
|
|
701
|
+
timerMetricAddDuration: updateTimerMetric,
|
|
702
|
+
});
|
|
703
|
+
// Our getRecord through aggregate-ui CRUD logging has moved
|
|
704
|
+
// to lds-network-adapter. We still need to respect the
|
|
705
|
+
// orgs environment setting
|
|
706
|
+
if (forceRecordTransactionsDisabled === false) {
|
|
707
|
+
ldsNetworkAdapterInstrument({
|
|
708
|
+
getRecordAggregateResolve: (cb) => {
|
|
709
|
+
const { recordId, apiName } = cb();
|
|
710
|
+
logCRUDLightningInteraction('read', {
|
|
711
|
+
recordId,
|
|
712
|
+
recordType: apiName,
|
|
713
|
+
state: 'SUCCESS',
|
|
714
|
+
});
|
|
657
715
|
},
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
return {
|
|
668
|
-
request: this.transformForSave(request),
|
|
669
|
-
context,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
isGetObjectInfoContextDependent(context, request) {
|
|
673
|
-
return (request.config.objectApiName && context.objectApiName === request.config.objectApiName);
|
|
716
|
+
getRecordAggregateReject: (cb) => {
|
|
717
|
+
const recordId = cb();
|
|
718
|
+
logCRUDLightningInteraction('read', {
|
|
719
|
+
recordId,
|
|
720
|
+
state: 'ERROR',
|
|
721
|
+
});
|
|
722
|
+
},
|
|
723
|
+
});
|
|
674
724
|
}
|
|
725
|
+
withRegistration('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
|
|
675
726
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
727
|
+
/**
|
|
728
|
+
* Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
|
|
729
|
+
*
|
|
730
|
+
* @param luvio The Luvio instance to instrument.
|
|
731
|
+
* @param store The InMemoryStore to instrument.
|
|
732
|
+
*/
|
|
733
|
+
function setupInstrumentation(luvio, store) {
|
|
734
|
+
setupInstrumentation$1(luvio, store);
|
|
735
|
+
setAuraInstrumentationHooks();
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
|
|
739
|
+
* disable all crud operations if it is on.
|
|
740
|
+
* @param eventSource - Source of the logging event.
|
|
741
|
+
* @param attributes - Free form object of attributes to log.
|
|
742
|
+
*/
|
|
743
|
+
function logCRUDLightningInteraction(eventSource, attributes) {
|
|
744
|
+
interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
|
|
686
745
|
}
|
|
746
|
+
const instrumentation = new Instrumentation();
|
|
687
747
|
|
|
688
|
-
class
|
|
689
|
-
constructor(
|
|
690
|
-
this.
|
|
691
|
-
this.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
getObjectInfo: new GetObjectInfoRequestStrategy(),
|
|
697
|
-
getObjectInfos: new GetObjectInfosRequestStrategy(),
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
reduceRequests(requests) {
|
|
701
|
-
return Object.values(this.requestStrategies)
|
|
702
|
-
.map((strategy) => strategy.reduce(requests))
|
|
703
|
-
.flat();
|
|
748
|
+
class ApplicationPredictivePrefetcher {
|
|
749
|
+
constructor(context, repository, requestRunner) {
|
|
750
|
+
this.repository = repository;
|
|
751
|
+
this.requestRunner = requestRunner;
|
|
752
|
+
this.isRecording = false;
|
|
753
|
+
this.queuedPredictionRequests = [];
|
|
754
|
+
this._context = context;
|
|
755
|
+
this.page = this.getPage();
|
|
704
756
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
return Promise.resolve(adapterFactory(this.luvio)(request.config)).then();
|
|
709
|
-
}
|
|
710
|
-
return Promise.resolve(undefined);
|
|
757
|
+
set context(value) {
|
|
758
|
+
this._context = value;
|
|
759
|
+
this.page = this.getPage();
|
|
711
760
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
class InMemoryPrefetchStorage {
|
|
715
|
-
constructor() {
|
|
716
|
-
this.data = {};
|
|
761
|
+
get context() {
|
|
762
|
+
return this._context;
|
|
717
763
|
}
|
|
718
|
-
|
|
719
|
-
this.
|
|
720
|
-
|
|
764
|
+
async stopRecording() {
|
|
765
|
+
this.isRecording = false;
|
|
766
|
+
await this.repository.flushRequestsToStorage();
|
|
721
767
|
}
|
|
722
|
-
|
|
723
|
-
|
|
768
|
+
startRecording() {
|
|
769
|
+
this.isRecording = true;
|
|
770
|
+
this.repository.clearRequestBuffer();
|
|
724
771
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
debugLogging: false,
|
|
736
|
-
version: 2,
|
|
737
|
-
};
|
|
738
|
-
function buildAuraPrefetchStorage(options = {}) {
|
|
739
|
-
const auraStorage = createStorage({
|
|
740
|
-
...DEFAULT_STORAGE_OPTIONS,
|
|
741
|
-
...options,
|
|
742
|
-
});
|
|
743
|
-
const inMemoryStorage = new InMemoryPrefetchStorage();
|
|
744
|
-
if (auraStorage === null) {
|
|
745
|
-
return inMemoryStorage;
|
|
772
|
+
saveRequest(request) {
|
|
773
|
+
if (!this.isRecording) {
|
|
774
|
+
return Promise.resolve();
|
|
775
|
+
}
|
|
776
|
+
return executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_SAVE_REQUEST, (_act) => {
|
|
777
|
+
const { request: requestToSave, context } = this.page.buildSaveRequestData(request);
|
|
778
|
+
// No need to differentiate from predictions requests because these
|
|
779
|
+
// are made from the adapters factory, which are not prediction aware.
|
|
780
|
+
return this.repository.saveRequest(context, requestToSave);
|
|
781
|
+
}, PDL_EXECUTE_ASYNC_OPTIONS);
|
|
746
782
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
* in overwrites, not graceful merges of predictions.
|
|
762
|
-
* 2. If whoever is consuming this tries to get and run predictions before this is done loading,
|
|
763
|
-
* then they will (potentially incorrectly) think that we don't have any predictions.
|
|
764
|
-
*/
|
|
765
|
-
auraStorage.getAll().then((results) => {
|
|
766
|
-
ObjectKeys(results).forEach((key) => this.inMemoryStorage.set(key, results[key]));
|
|
767
|
-
});
|
|
783
|
+
async predict() {
|
|
784
|
+
const exactPageRequests = (await this.repository.getPageRequests(this.context)) || [];
|
|
785
|
+
const similarPageRequests = await this.getSimilarPageRequests();
|
|
786
|
+
const alwaysRequests = this.page.getAlwaysRunRequests();
|
|
787
|
+
const predictedRequests = [
|
|
788
|
+
...alwaysRequests,
|
|
789
|
+
...this.requestRunner.reduceRequests([
|
|
790
|
+
...exactPageRequests,
|
|
791
|
+
...similarPageRequests,
|
|
792
|
+
...this.page.getAlwaysRunRequests(),
|
|
793
|
+
]),
|
|
794
|
+
];
|
|
795
|
+
this.queuedPredictionRequests.push(...predictedRequests);
|
|
796
|
+
return Promise.all(predictedRequests.map((request) => this.requestRunner.runRequest(request))).then();
|
|
768
797
|
}
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
this.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
return inMemoryResult;
|
|
798
|
+
hasPredictions() {
|
|
799
|
+
const exactPageRequests = this.repository.getPageRequests(this.context) || [];
|
|
800
|
+
const similarPageRequests = this.page.similarContext !== undefined
|
|
801
|
+
? this.repository.getPageRequests(this.page.similarContext)
|
|
802
|
+
: [];
|
|
803
|
+
return exactPageRequests.length > 0 || similarPageRequests.length > 0;
|
|
778
804
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
805
|
+
getSimilarPageRequests() {
|
|
806
|
+
let resolvedSimilarPageRequests = [];
|
|
807
|
+
if (this.page.similarContext !== undefined) {
|
|
808
|
+
const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
|
|
809
|
+
if (similarPageRequests !== undefined) {
|
|
810
|
+
resolvedSimilarPageRequests = similarPageRequests.map((request) => this.page.resolveSimilarRequest(request));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return resolvedSimilarPageRequests;
|
|
782
814
|
}
|
|
783
815
|
}
|
|
784
816
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
* W-8379680
|
|
802
|
-
* Counter for number of getApex requests.
|
|
803
|
-
*/
|
|
804
|
-
const GET_APEX_REQUEST_COUNT = {
|
|
805
|
-
get() {
|
|
806
|
-
return {
|
|
807
|
-
owner: OBSERVABILITY_NAMESPACE,
|
|
808
|
-
name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
|
|
809
|
-
};
|
|
810
|
-
},
|
|
811
|
-
};
|
|
812
|
-
/**
|
|
813
|
-
* W-8828410
|
|
814
|
-
* Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
|
|
815
|
-
*/
|
|
816
|
-
const TOTAL_ADAPTER_ERROR_COUNT = {
|
|
817
|
-
get() {
|
|
818
|
-
return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
|
|
819
|
-
},
|
|
820
|
-
};
|
|
821
|
-
/**
|
|
822
|
-
* W-8828410
|
|
823
|
-
* Counter for the number of invocations made into LDS by a wire adapter.
|
|
824
|
-
*/
|
|
825
|
-
const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
|
|
826
|
-
get() {
|
|
827
|
-
return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
|
|
828
|
-
},
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
const { create, keys } = Object;
|
|
832
|
-
const { isArray } = Array;
|
|
833
|
-
const { stringify } = JSON;
|
|
817
|
+
class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
|
|
818
|
+
constructor(context, repository, requestRunner,
|
|
819
|
+
// These strategies need to be in sync with the "predictiveDataLoadCapable" list
|
|
820
|
+
// from scripts/lds-uiapi-plugin.js
|
|
821
|
+
requestStrategies) {
|
|
822
|
+
super(context, repository, requestRunner);
|
|
823
|
+
this.requestStrategies = requestStrategies;
|
|
824
|
+
this.page = this.getPage();
|
|
825
|
+
}
|
|
826
|
+
getPage() {
|
|
827
|
+
if (RecordHomePage.handlesContext(this.context)) {
|
|
828
|
+
return new RecordHomePage(this.context, this.requestStrategies);
|
|
829
|
+
}
|
|
830
|
+
return new LexDefaultPage(this.context);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
834
833
|
|
|
834
|
+
// Copy-pasted from adapter-utils. This util should be extracted from generated code and imported in prefetch repository.
|
|
835
|
+
const { keys: ObjectKeys$1 } = Object;
|
|
836
|
+
const { stringify: JSONStringify } = JSON;
|
|
837
|
+
const { isArray: ArrayIsArray } = Array;
|
|
835
838
|
/**
|
|
836
839
|
* A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
|
|
837
840
|
* This is needed because insertion order for JSON.stringify(object) affects output:
|
|
@@ -839,7 +842,6 @@ const { stringify } = JSON;
|
|
|
839
842
|
* "{"a":1,"b":2}"
|
|
840
843
|
* JSON.stringify({b: 2, a: 1})
|
|
841
844
|
* "{"b":2,"a":1}"
|
|
842
|
-
* Modified from the apex implementation to sort arrays non-destructively.
|
|
843
845
|
* @param data Data to be JSON-stringified.
|
|
844
846
|
* @returns JSON.stringified value with consistent ordering of keys.
|
|
845
847
|
*/
|
|
@@ -849,512 +851,517 @@ function stableJSONStringify(node) {
|
|
|
849
851
|
// eslint-disable-next-line no-param-reassign
|
|
850
852
|
node = node.toJSON();
|
|
851
853
|
}
|
|
852
|
-
if (node === undefined) {
|
|
853
|
-
return;
|
|
854
|
+
if (node === undefined) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (typeof node === 'number') {
|
|
858
|
+
return isFinite(node) ? '' + node : 'null';
|
|
859
|
+
}
|
|
860
|
+
if (typeof node !== 'object') {
|
|
861
|
+
return JSONStringify(node);
|
|
862
|
+
}
|
|
863
|
+
let i;
|
|
864
|
+
let out;
|
|
865
|
+
if (ArrayIsArray(node)) {
|
|
866
|
+
out = '[';
|
|
867
|
+
for (i = 0; i < node.length; i++) {
|
|
868
|
+
if (i) {
|
|
869
|
+
out += ',';
|
|
870
|
+
}
|
|
871
|
+
out += stableJSONStringify(node[i]) || 'null';
|
|
872
|
+
}
|
|
873
|
+
return out + ']';
|
|
874
|
+
}
|
|
875
|
+
if (node === null) {
|
|
876
|
+
return 'null';
|
|
877
|
+
}
|
|
878
|
+
const keys = ObjectKeys$1(node).sort();
|
|
879
|
+
out = '';
|
|
880
|
+
for (i = 0; i < keys.length; i++) {
|
|
881
|
+
const key = keys[i];
|
|
882
|
+
const value = stableJSONStringify(node[key]);
|
|
883
|
+
if (!value) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (out) {
|
|
887
|
+
out += ',';
|
|
888
|
+
}
|
|
889
|
+
out += JSONStringify(key) + ':' + value;
|
|
890
|
+
}
|
|
891
|
+
return '{' + out + '}';
|
|
892
|
+
}
|
|
893
|
+
function isObject(obj) {
|
|
894
|
+
return obj !== null && typeof obj === 'object';
|
|
895
|
+
}
|
|
896
|
+
function deepEquals(objA, objB) {
|
|
897
|
+
if (objA === objB)
|
|
898
|
+
return true;
|
|
899
|
+
if (objA instanceof Date && objB instanceof Date)
|
|
900
|
+
return objA.getTime() === objB.getTime();
|
|
901
|
+
// If one of them is not an object, they are not deeply equal
|
|
902
|
+
if (!isObject(objA) || !isObject(objB))
|
|
903
|
+
return false;
|
|
904
|
+
// Filter out keys set as undefined, we can compare undefined as equals.
|
|
905
|
+
const keysA = ObjectKeys$1(objA).filter((key) => objA[key] !== undefined);
|
|
906
|
+
const keysB = ObjectKeys$1(objB).filter((key) => objB[key] !== undefined);
|
|
907
|
+
// If the objects do not have the same set of keys, they are not deeply equal
|
|
908
|
+
if (keysA.length !== keysB.length)
|
|
909
|
+
return false;
|
|
910
|
+
for (const key of keysA) {
|
|
911
|
+
const valA = objA[key];
|
|
912
|
+
const valB = objB[key];
|
|
913
|
+
const areObjects = isObject(valA) && isObject(valB);
|
|
914
|
+
// If both values are objects, recursively compare them
|
|
915
|
+
if (areObjects && !deepEquals(valA, valB))
|
|
916
|
+
return false;
|
|
917
|
+
// If only one value is an object or if the values are not strictly equal, they are not deeply equal
|
|
918
|
+
if (!areObjects && valA !== valB)
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
class PrefetchRepository {
|
|
925
|
+
constructor(storage) {
|
|
926
|
+
this.storage = storage;
|
|
927
|
+
this.requestBuffer = new Map();
|
|
928
|
+
}
|
|
929
|
+
clearRequestBuffer() {
|
|
930
|
+
this.requestBuffer.clear();
|
|
931
|
+
}
|
|
932
|
+
async flushRequestsToStorage() {
|
|
933
|
+
for (const [id, batch] of this.requestBuffer) {
|
|
934
|
+
const rawPage = await this.storage.get(id);
|
|
935
|
+
const page = rawPage === undefined ? { id, requests: [] } : rawPage;
|
|
936
|
+
batch.forEach(({ request, requestTime }) => {
|
|
937
|
+
let existingRequestEntry = page.requests.find(({ request: storedRequest }) => deepEquals(storedRequest, request));
|
|
938
|
+
if (existingRequestEntry === undefined) {
|
|
939
|
+
existingRequestEntry = { request, requestMetadata: { requestTimes: [] } };
|
|
940
|
+
page.requests.push(existingRequestEntry);
|
|
941
|
+
}
|
|
942
|
+
existingRequestEntry.requestMetadata.requestTimes.push(requestTime);
|
|
943
|
+
});
|
|
944
|
+
await this.storage.set(id, page);
|
|
945
|
+
}
|
|
946
|
+
this.clearRequestBuffer();
|
|
947
|
+
}
|
|
948
|
+
async saveRequest(key, request) {
|
|
949
|
+
const identifier = stableJSONStringify(key);
|
|
950
|
+
const batchForKey = this.requestBuffer.get(identifier) || [];
|
|
951
|
+
batchForKey.push({
|
|
952
|
+
request,
|
|
953
|
+
requestTime: Date.now(),
|
|
954
|
+
});
|
|
955
|
+
this.requestBuffer.set(identifier, batchForKey);
|
|
956
|
+
}
|
|
957
|
+
getPage(key) {
|
|
958
|
+
const identifier = stableJSONStringify(key);
|
|
959
|
+
return this.storage.get(identifier);
|
|
960
|
+
}
|
|
961
|
+
getPageRequests(key) {
|
|
962
|
+
const page = this.getPage(key);
|
|
963
|
+
if (page === undefined) {
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
return page.requests.map((requestEntry) => requestEntry.request);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
class RequestStrategy {
|
|
971
|
+
transformForSave(request) {
|
|
972
|
+
return request;
|
|
973
|
+
}
|
|
974
|
+
reduce(requests) {
|
|
975
|
+
return requests;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
class LuvioAdapterRequestStrategy extends RequestStrategy {
|
|
980
|
+
transformForSave(request) {
|
|
981
|
+
return request;
|
|
982
|
+
}
|
|
983
|
+
filterRequests(unfilteredRequests) {
|
|
984
|
+
return unfilteredRequests.filter((request) => request.adapterName === this.adapterName);
|
|
985
|
+
}
|
|
986
|
+
reduce(unfilteredRequests) {
|
|
987
|
+
const requests = this.filterRequests(unfilteredRequests);
|
|
988
|
+
const visitedRequests = new Set();
|
|
989
|
+
const reducedRequests = [];
|
|
990
|
+
for (let i = 0, n = requests.length; i < n; i++) {
|
|
991
|
+
const currentRequest = requests[i];
|
|
992
|
+
if (!visitedRequests.has(currentRequest)) {
|
|
993
|
+
const combinedRequest = { ...currentRequest };
|
|
994
|
+
for (let j = i + 1; j < n; j++) {
|
|
995
|
+
const hasNotBeenVisited = !visitedRequests.has(requests[j]);
|
|
996
|
+
const canCombineConfigs = this.canCombine(combinedRequest.config, requests[j].config);
|
|
997
|
+
if (hasNotBeenVisited && canCombineConfigs) {
|
|
998
|
+
combinedRequest.config = this.combineRequests(combinedRequest.config, requests[j].config);
|
|
999
|
+
visitedRequests.add(requests[j]);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
reducedRequests.push(combinedRequest);
|
|
1003
|
+
visitedRequests.add(currentRequest);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return reducedRequests;
|
|
1007
|
+
}
|
|
1008
|
+
canCombine(_reqA, _reqB) {
|
|
1009
|
+
// By default, all requests are not comibinable
|
|
1010
|
+
return false;
|
|
854
1011
|
}
|
|
855
|
-
|
|
856
|
-
|
|
1012
|
+
combineRequests(reqA, _reqB) {
|
|
1013
|
+
// By default, this should never be called since requests aren't combinable
|
|
1014
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1015
|
+
throw new Error('Not implemented');
|
|
1016
|
+
}
|
|
1017
|
+
return reqA;
|
|
857
1018
|
}
|
|
858
|
-
|
|
859
|
-
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function normalizeRecordIds$1(recordIds) {
|
|
1022
|
+
if (!Array.isArray(recordIds)) {
|
|
1023
|
+
return [recordIds];
|
|
860
1024
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
for (i = 0; i < node.length; i++) {
|
|
869
|
-
if (i) {
|
|
870
|
-
out += ',';
|
|
871
|
-
}
|
|
872
|
-
out += stableJSONStringify(node[i]) || 'null';
|
|
873
|
-
}
|
|
874
|
-
return out + ']';
|
|
1025
|
+
return recordIds;
|
|
1026
|
+
}
|
|
1027
|
+
class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
|
|
1028
|
+
constructor() {
|
|
1029
|
+
super(...arguments);
|
|
1030
|
+
this.adapterName = 'getRecordAvatars';
|
|
1031
|
+
this.adapterFactory = getRecordAvatarsAdapterFactory;
|
|
875
1032
|
}
|
|
876
|
-
|
|
877
|
-
return
|
|
1033
|
+
buildConcreteRequest(similarRequest, context) {
|
|
1034
|
+
return {
|
|
1035
|
+
...similarRequest,
|
|
1036
|
+
config: {
|
|
1037
|
+
...similarRequest.config,
|
|
1038
|
+
recordIds: [context.recordId],
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
878
1041
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1042
|
+
buildGetRecordAvatarsSaveRequestData(similarContext, context, request) {
|
|
1043
|
+
if (this.isGetRecordAvatarsRequestContextDependent(context, request)) {
|
|
1044
|
+
return {
|
|
1045
|
+
request: this.transformForSave({
|
|
1046
|
+
...request,
|
|
1047
|
+
config: {
|
|
1048
|
+
...request.config,
|
|
1049
|
+
recordIds: ['*'],
|
|
1050
|
+
},
|
|
1051
|
+
}),
|
|
1052
|
+
context: similarContext,
|
|
1053
|
+
};
|
|
889
1054
|
}
|
|
890
|
-
|
|
1055
|
+
return {
|
|
1056
|
+
request: this.transformForSave(request),
|
|
1057
|
+
context,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
isGetRecordAvatarsRequestContextDependent(context, request) {
|
|
1061
|
+
return (request.config.recordIds &&
|
|
1062
|
+
(context.recordId === request.config.recordIds || // some may set this as string instead of array
|
|
1063
|
+
(request.config.recordIds.length === 1 &&
|
|
1064
|
+
request.config.recordIds[0] === context.recordId)));
|
|
1065
|
+
}
|
|
1066
|
+
canCombine(reqA, reqB) {
|
|
1067
|
+
return reqA.formFactor === reqB.formFactor;
|
|
1068
|
+
}
|
|
1069
|
+
combineRequests(reqA, reqB) {
|
|
1070
|
+
const combined = { ...reqA };
|
|
1071
|
+
combined.recordIds = Array.from(new Set([...normalizeRecordIds$1(reqA.recordIds), ...normalizeRecordIds$1(reqB.recordIds)]));
|
|
1072
|
+
return combined;
|
|
891
1073
|
}
|
|
892
|
-
return '{' + out + '}';
|
|
893
|
-
}
|
|
894
|
-
function isPromise(value) {
|
|
895
|
-
// check for Thenable due to test frameworks using custom Promise impls
|
|
896
|
-
return value !== null && value.then !== undefined;
|
|
897
1074
|
}
|
|
898
1075
|
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
const REFRESH_APEX_KEY = 'refreshApex';
|
|
902
|
-
const REFRESH_UIAPI_KEY = 'refreshUiApi';
|
|
903
|
-
const SUPPORTED_KEY = 'refreshSupported';
|
|
904
|
-
const UNSUPPORTED_KEY = 'refreshUnsupported';
|
|
905
|
-
const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
|
|
906
|
-
const REFRESH_EVENTTYPE = 'system';
|
|
907
|
-
const REFRESH_PAYLOAD_TARGET = 'adapters';
|
|
908
|
-
const REFRESH_PAYLOAD_SCOPE = 'lds';
|
|
909
|
-
const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
|
|
910
|
-
const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
|
|
911
|
-
const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
|
|
912
|
-
const NAMESPACE = 'lds';
|
|
913
|
-
const NETWORK_TRANSACTION_NAME = 'lds-network';
|
|
914
|
-
const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
|
|
915
|
-
// Aggregate Cache Stats and Metrics for all getApex invocations
|
|
916
|
-
const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
|
|
917
|
-
const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
|
|
918
|
-
// Observability (READS)
|
|
919
|
-
const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
|
|
920
|
-
const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
|
|
921
|
-
const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
|
|
922
|
-
class Instrumentation {
|
|
1076
|
+
const COERCE_FIELD_ID_ARRAY_OPTIONS = { onlyQualifiedFieldNames: true };
|
|
1077
|
+
class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
|
|
923
1078
|
constructor() {
|
|
924
|
-
|
|
925
|
-
this.
|
|
926
|
-
this.
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1079
|
+
super(...arguments);
|
|
1080
|
+
this.adapterName = 'getRecord';
|
|
1081
|
+
this.adapterFactory = getRecordAdapterFactory;
|
|
1082
|
+
}
|
|
1083
|
+
buildConcreteRequest(similarRequest, context) {
|
|
1084
|
+
return {
|
|
1085
|
+
...similarRequest,
|
|
1086
|
+
config: {
|
|
1087
|
+
...similarRequest.config,
|
|
1088
|
+
recordId: context.recordId,
|
|
1089
|
+
},
|
|
932
1090
|
};
|
|
933
|
-
this.lastRefreshApiCall = null;
|
|
934
|
-
this.weakEtagZeroEvents = {};
|
|
935
|
-
this.adapterCacheMisses = new LRUCache(250);
|
|
936
|
-
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
937
|
-
window.addEventListener('beforeunload', () => {
|
|
938
|
-
if (keys(this.weakEtagZeroEvents).length > 0) {
|
|
939
|
-
perfStart(NETWORK_TRANSACTION_NAME);
|
|
940
|
-
perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
|
|
945
1091
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const ttlMissStats = isGetApexAdapter
|
|
960
|
-
? getApexTtlCacheStats
|
|
961
|
-
: registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
|
|
962
|
-
/**
|
|
963
|
-
* W-8076905
|
|
964
|
-
* Dynamically generated metric. Simple counter for all requests made by this adapter.
|
|
965
|
-
*/
|
|
966
|
-
const wireAdapterRequestMetric = isGetApexAdapter
|
|
967
|
-
? getApexRequestCountMetric
|
|
968
|
-
: counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
|
|
969
|
-
const instrumentedAdapter = (config, requestContext) => {
|
|
970
|
-
// increment overall and adapter request metrics
|
|
971
|
-
wireAdapterRequestMetric.increment(1);
|
|
972
|
-
totalAdapterRequestSuccessMetric.increment(1);
|
|
973
|
-
// execute adapter logic
|
|
974
|
-
const result = adapter(config, requestContext);
|
|
975
|
-
// In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
|
|
976
|
-
// (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
|
|
977
|
-
//
|
|
978
|
-
// Note: we can't do a plain instanceof check for a promise here since the Promise may
|
|
979
|
-
// originate from another javascript realm (for example: in jest test). Instead we use a
|
|
980
|
-
// duck-typing approach by checking if the result has a then property.
|
|
981
|
-
//
|
|
982
|
-
// For adapters without persistent store:
|
|
983
|
-
// - total cache hit ratio:
|
|
984
|
-
// [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
|
|
985
|
-
// For adapters with persistent store:
|
|
986
|
-
// - in-memory cache hit ratio:
|
|
987
|
-
// [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
|
|
988
|
-
// - total cache hit ratio:
|
|
989
|
-
// ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
|
|
990
|
-
// if result === null then config is insufficient/invalid so do not log
|
|
991
|
-
if (isPromise(result)) {
|
|
992
|
-
stats.logMisses();
|
|
993
|
-
if (ttl !== undefined) {
|
|
994
|
-
this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
else if (result !== null) {
|
|
998
|
-
stats.logHits();
|
|
999
|
-
}
|
|
1000
|
-
return result;
|
|
1092
|
+
transformForSave(request) {
|
|
1093
|
+
if (request.config.fields === undefined && request.config.optionalFields === undefined) {
|
|
1094
|
+
return request;
|
|
1095
|
+
}
|
|
1096
|
+
let fields = coerceFieldIdArray(request.config.fields, COERCE_FIELD_ID_ARRAY_OPTIONS) || [];
|
|
1097
|
+
let optionalFields = coerceFieldIdArray(request.config.optionalFields, COERCE_FIELD_ID_ARRAY_OPTIONS) || [];
|
|
1098
|
+
return {
|
|
1099
|
+
...request,
|
|
1100
|
+
config: {
|
|
1101
|
+
...request.config,
|
|
1102
|
+
fields: undefined,
|
|
1103
|
+
optionalFields: [...fields, ...optionalFields],
|
|
1104
|
+
},
|
|
1001
1105
|
};
|
|
1002
|
-
// Set the name property on the function for debugging purposes.
|
|
1003
|
-
Object.defineProperty(instrumentedAdapter, 'name', {
|
|
1004
|
-
value: name + '__instrumented',
|
|
1005
|
-
});
|
|
1006
|
-
return instrumentAdapter(instrumentedAdapter, metadata);
|
|
1007
1106
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
1023
|
-
if (duration > ttl) {
|
|
1024
|
-
ttlMissStats.logMisses();
|
|
1025
|
-
}
|
|
1107
|
+
canCombine(reqA, reqB) {
|
|
1108
|
+
// must be same record and
|
|
1109
|
+
return (reqA.recordId === reqB.recordId &&
|
|
1110
|
+
// both requests are fields requests
|
|
1111
|
+
(reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
|
|
1112
|
+
(reqB.fields !== undefined || reqB.optionalFields !== undefined));
|
|
1113
|
+
}
|
|
1114
|
+
combineRequests(reqA, reqB) {
|
|
1115
|
+
const fields = new Set();
|
|
1116
|
+
const optionalFields = new Set();
|
|
1117
|
+
if (reqA.fields !== undefined) {
|
|
1118
|
+
reqA.fields.forEach((field) => fields.add(field));
|
|
1119
|
+
}
|
|
1120
|
+
if (reqB.fields !== undefined) {
|
|
1121
|
+
reqB.fields.forEach((field) => fields.add(field));
|
|
1026
1122
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
* Injected to LDS for Luvio specific instrumentation.
|
|
1030
|
-
*
|
|
1031
|
-
* @param context The transaction context.
|
|
1032
|
-
*/
|
|
1033
|
-
instrumentLuvio(context) {
|
|
1034
|
-
instrumentLuvio(context);
|
|
1035
|
-
if (this.isRefreshAdapterEvent(context)) {
|
|
1036
|
-
this.aggregateRefreshAdapterEvents(context);
|
|
1123
|
+
if (reqA.optionalFields !== undefined) {
|
|
1124
|
+
reqA.optionalFields.forEach((field) => optionalFields.add(field));
|
|
1037
1125
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1126
|
+
if (reqB.optionalFields !== undefined) {
|
|
1127
|
+
reqB.optionalFields.forEach((field) => optionalFields.add(field));
|
|
1040
1128
|
}
|
|
1041
|
-
|
|
1129
|
+
return {
|
|
1130
|
+
recordId: reqA.recordId,
|
|
1131
|
+
fields: Array.from(fields),
|
|
1132
|
+
optionalFields: Array.from(optionalFields),
|
|
1133
|
+
};
|
|
1042
1134
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
|
|
1138
|
+
constructor() {
|
|
1139
|
+
super(...arguments);
|
|
1140
|
+
this.adapterName = 'getRecords';
|
|
1141
|
+
this.adapterFactory = getRecordsAdapterFactory;
|
|
1050
1142
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1143
|
+
buildConcreteRequest(similarRequest, context) {
|
|
1144
|
+
return {
|
|
1145
|
+
...similarRequest,
|
|
1146
|
+
config: {
|
|
1147
|
+
...similarRequest.config,
|
|
1148
|
+
records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
|
|
1149
|
+
},
|
|
1150
|
+
};
|
|
1058
1151
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
* @param error if dispatchResourceRequest fails for any reason
|
|
1065
|
-
*/
|
|
1066
|
-
notifyChangeNetwork(uniqueWeakEtags, error) {
|
|
1067
|
-
perfStart(NETWORK_TRANSACTION_NAME);
|
|
1068
|
-
if (error === true) {
|
|
1069
|
-
perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
|
|
1070
|
-
}
|
|
1071
|
-
else {
|
|
1072
|
-
perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
|
|
1073
|
-
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function normalizeRecordIds(recordIds) {
|
|
1155
|
+
if (!ArrayIsArray(recordIds)) {
|
|
1156
|
+
return [recordIds];
|
|
1074
1157
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1158
|
+
return recordIds;
|
|
1159
|
+
}
|
|
1160
|
+
function normalizeApiNames(apiNames) {
|
|
1161
|
+
if (apiNames === undefined || apiNames === null) {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
return ArrayIsArray(apiNames) ? apiNames : [apiNames];
|
|
1165
|
+
}
|
|
1166
|
+
class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
|
|
1167
|
+
constructor() {
|
|
1168
|
+
super(...arguments);
|
|
1169
|
+
this.adapterName = 'getRecordActions';
|
|
1170
|
+
this.adapterFactory = getRecordActionsAdapterFactory;
|
|
1171
|
+
}
|
|
1172
|
+
buildConcreteRequest(similarRequest, context) {
|
|
1173
|
+
return {
|
|
1174
|
+
...similarRequest,
|
|
1175
|
+
config: {
|
|
1176
|
+
...similarRequest.config,
|
|
1177
|
+
recordIds: [context.recordId],
|
|
1178
|
+
},
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
buildGetRecordActionsSaveRequestData(similarContext, context, request) {
|
|
1182
|
+
if (this.isGetRecordActionsRequestContextDependent(context, request)) {
|
|
1183
|
+
return {
|
|
1184
|
+
request: this.transformForSave({
|
|
1185
|
+
...request,
|
|
1186
|
+
config: {
|
|
1187
|
+
...request.config,
|
|
1188
|
+
recordIds: ['*'],
|
|
1189
|
+
},
|
|
1190
|
+
}),
|
|
1191
|
+
context: similarContext,
|
|
1085
1192
|
};
|
|
1086
1193
|
}
|
|
1087
|
-
|
|
1088
|
-
this.
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
|
|
1092
|
-
}
|
|
1194
|
+
return {
|
|
1195
|
+
request: this.transformForSave(request),
|
|
1196
|
+
context,
|
|
1197
|
+
};
|
|
1093
1198
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
|
|
1111
|
-
}
|
|
1112
|
-
else {
|
|
1113
|
-
this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
|
|
1117
|
-
this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
|
|
1118
|
-
}
|
|
1119
|
-
if (this.refreshAdapterEvents[adapterName] === undefined) {
|
|
1120
|
-
this.refreshAdapterEvents[adapterName] = 0;
|
|
1199
|
+
canCombine(reqA, reqB) {
|
|
1200
|
+
return (reqA.retrievalMode === reqB.retrievalMode &&
|
|
1201
|
+
reqA.formFactor === reqB.formFactor &&
|
|
1202
|
+
(reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
|
|
1203
|
+
(reqA.sections || []).toString() === (reqB.sections || []).toString());
|
|
1204
|
+
}
|
|
1205
|
+
combineRequests(reqA, reqB) {
|
|
1206
|
+
const combined = { ...reqA };
|
|
1207
|
+
// let's merge the recordIds
|
|
1208
|
+
combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
|
|
1209
|
+
if (combined.retrievalMode === 'ALL') {
|
|
1210
|
+
const combinedSet = new Set([
|
|
1211
|
+
...normalizeApiNames(combined.apiNames),
|
|
1212
|
+
...normalizeApiNames(reqB.apiNames),
|
|
1213
|
+
]);
|
|
1214
|
+
combined.apiNames = Array.from(combinedSet);
|
|
1121
1215
|
}
|
|
1122
|
-
|
|
1123
|
-
this.lastRefreshApiCall = null;
|
|
1216
|
+
return combined;
|
|
1124
1217
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
handleRefreshApiCall(apiName) {
|
|
1131
|
-
this.refreshApiCallEventStats[apiName] += 1;
|
|
1132
|
-
// set function call to be used with aggregateRefreshCalls
|
|
1133
|
-
this.lastRefreshApiCall = apiName;
|
|
1218
|
+
isGetRecordActionsRequestContextDependent(context, request) {
|
|
1219
|
+
return (request.config.recordIds &&
|
|
1220
|
+
(context.recordId === request.config.recordIds || // some may set this as string instead of array
|
|
1221
|
+
(request.config.recordIds.length === 1 &&
|
|
1222
|
+
request.config.recordIds[0] === context.recordId)));
|
|
1134
1223
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
|
|
1227
|
+
constructor() {
|
|
1228
|
+
super(...arguments);
|
|
1229
|
+
this.adapterName = 'getObjectInfo';
|
|
1230
|
+
this.adapterFactory = getObjectInfoAdapterFactory;
|
|
1231
|
+
}
|
|
1232
|
+
buildConcreteRequest(similarRequest, context) {
|
|
1233
|
+
return {
|
|
1234
|
+
...similarRequest,
|
|
1235
|
+
config: {
|
|
1236
|
+
...similarRequest.config,
|
|
1237
|
+
objectApiName: context.objectApiName,
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
buildGetObjectInfoSaveRequestData(similarContext, context, request) {
|
|
1242
|
+
if (this.isGetObjectInfoContextDependent(context, request)) {
|
|
1243
|
+
return {
|
|
1244
|
+
request: this.transformForSave(request),
|
|
1245
|
+
context: similarContext,
|
|
1246
|
+
};
|
|
1143
1247
|
}
|
|
1248
|
+
return {
|
|
1249
|
+
request: this.transformForSave(request),
|
|
1250
|
+
context,
|
|
1251
|
+
};
|
|
1144
1252
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1253
|
+
isGetObjectInfoContextDependent(context, request) {
|
|
1254
|
+
return (request.config.objectApiName && context.objectApiName === request.config.objectApiName);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
class GetObjectInfosRequestStrategy extends LuvioAdapterRequestStrategy {
|
|
1259
|
+
constructor() {
|
|
1260
|
+
super(...arguments);
|
|
1261
|
+
this.adapterName = 'getObjectInfos';
|
|
1262
|
+
this.adapterFactory = getObjectInfosAdapterFactory;
|
|
1263
|
+
}
|
|
1264
|
+
buildConcreteRequest(similarRequest) {
|
|
1265
|
+
return similarRequest;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
class LexRequestRunner {
|
|
1270
|
+
constructor(luvio) {
|
|
1271
|
+
this.luvio = luvio;
|
|
1272
|
+
this.requestStrategies = {
|
|
1273
|
+
getRecord: new GetRecordRequestStrategy(),
|
|
1274
|
+
getRecords: new GetRecordsRequestStrategy(),
|
|
1275
|
+
getRecordActions: new GetRecordActionsRequestStrategy(),
|
|
1276
|
+
getRecordAvatars: new GetRecordAvatarsRequestStrategy(),
|
|
1277
|
+
getObjectInfo: new GetObjectInfoRequestStrategy(),
|
|
1278
|
+
getObjectInfos: new GetObjectInfosRequestStrategy(),
|
|
1155
1279
|
};
|
|
1156
|
-
this.lastRefreshApiCall = null;
|
|
1157
1280
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
*
|
|
1163
|
-
* @param context The transaction context.
|
|
1164
|
-
*
|
|
1165
|
-
* Note: Short-lived metric candidate, remove at the end of 230
|
|
1166
|
-
*/
|
|
1167
|
-
incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
|
|
1168
|
-
let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
|
|
1169
|
-
if (apiNameChangeCounter === undefined) {
|
|
1170
|
-
apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
|
|
1171
|
-
this.recordApiNameChangeCounters[existingApiName] = apiNameChangeCounter;
|
|
1172
|
-
}
|
|
1173
|
-
apiNameChangeCounter.increment(1);
|
|
1281
|
+
reduceRequests(requests) {
|
|
1282
|
+
return Object.values(this.requestStrategies)
|
|
1283
|
+
.map((strategy) => strategy.reduce(requests))
|
|
1284
|
+
.flat();
|
|
1174
1285
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
* @param context The transaction context.
|
|
1180
|
-
*/
|
|
1181
|
-
incrementAdapterRequestErrorCount(context) {
|
|
1182
|
-
// We are consolidating all apex adapter instrumentation calls under a single key
|
|
1183
|
-
const adapterName = normalizeAdapterName(context.adapterName);
|
|
1184
|
-
let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
|
|
1185
|
-
if (adapterRequestErrorCounter === undefined) {
|
|
1186
|
-
adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
|
|
1187
|
-
this.adapterUnfulfilledErrorCounters[adapterName] = adapterRequestErrorCounter;
|
|
1286
|
+
runRequest(request) {
|
|
1287
|
+
if (request.adapterName in this.requestStrategies) {
|
|
1288
|
+
const adapterFactory = this.requestStrategies[request.adapterName].adapterFactory;
|
|
1289
|
+
return Promise.resolve(adapterFactory(this.luvio)(request.config)).then();
|
|
1188
1290
|
}
|
|
1189
|
-
|
|
1190
|
-
totalAdapterErrorMetric.increment(1);
|
|
1291
|
+
return Promise.resolve(undefined);
|
|
1191
1292
|
}
|
|
1192
1293
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1294
|
+
|
|
1295
|
+
class InMemoryPrefetchStorage {
|
|
1296
|
+
constructor() {
|
|
1297
|
+
this.data = {};
|
|
1197
1298
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
},
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
/**
|
|
1205
|
-
* Returns whether adapter is an Apex one or not.
|
|
1206
|
-
* @param adapterName The name of the adapter.
|
|
1207
|
-
*/
|
|
1208
|
-
function isApexAdapter(adapterName) {
|
|
1209
|
-
return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
|
|
1213
|
-
* API family, if supplied. Example: `UiApi.getRecord`.
|
|
1214
|
-
*
|
|
1215
|
-
* Note: If you are adding additional logging that can come from getApex adapter contexts that provide
|
|
1216
|
-
* the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
|
|
1217
|
-
* ensure to call this method to normalize all logging to 'getApex'. This
|
|
1218
|
-
* is because Argus has a 50k key cardinality limit. More context: W-8379680.
|
|
1219
|
-
*
|
|
1220
|
-
* @param adapterName The name of the adapter.
|
|
1221
|
-
* @param apiFamily The API family of the adapter.
|
|
1222
|
-
*/
|
|
1223
|
-
function normalizeAdapterName(adapterName, apiFamily) {
|
|
1224
|
-
if (isApexAdapter(adapterName)) {
|
|
1225
|
-
return NORMALIZED_APEX_ADAPTER_NAME;
|
|
1299
|
+
set(key, value) {
|
|
1300
|
+
this.data[key] = value;
|
|
1301
|
+
return Promise.resolve();
|
|
1226
1302
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const timerMetricTracker = create(null);
|
|
1230
|
-
/**
|
|
1231
|
-
* Calls instrumentation/service telemetry timer
|
|
1232
|
-
* @param name Name of the metric
|
|
1233
|
-
* @param duration number to update backing percentile histogram, negative numbers ignored
|
|
1234
|
-
*/
|
|
1235
|
-
function updateTimerMetric(name, duration) {
|
|
1236
|
-
let metric = timerMetricTracker[name];
|
|
1237
|
-
if (metric === undefined) {
|
|
1238
|
-
metric = timer(createMetricsKey(NAMESPACE, name));
|
|
1239
|
-
timerMetricTracker[name] = metric;
|
|
1303
|
+
get(key) {
|
|
1304
|
+
return this.data[key];
|
|
1240
1305
|
}
|
|
1241
|
-
timerMetricAddDuration(metric, duration);
|
|
1242
1306
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1307
|
+
|
|
1308
|
+
const { keys: ObjectKeys } = Object;
|
|
1309
|
+
const DEFAULT_STORAGE_OPTIONS = {
|
|
1310
|
+
name: 'ldsPredictiveLoading',
|
|
1311
|
+
persistent: true,
|
|
1312
|
+
secure: true,
|
|
1313
|
+
maxSize: 7 * 1024 * 1024,
|
|
1314
|
+
expiration: 12 * 60 * 60,
|
|
1315
|
+
clearOnInit: false,
|
|
1316
|
+
debugLogging: false,
|
|
1317
|
+
version: 2,
|
|
1318
|
+
};
|
|
1319
|
+
function buildAuraPrefetchStorage(options = {}) {
|
|
1320
|
+
const auraStorage = createStorage({
|
|
1321
|
+
...DEFAULT_STORAGE_OPTIONS,
|
|
1322
|
+
...options,
|
|
1323
|
+
});
|
|
1324
|
+
const inMemoryStorage = new InMemoryPrefetchStorage();
|
|
1325
|
+
if (auraStorage === null) {
|
|
1326
|
+
return inMemoryStorage;
|
|
1247
1327
|
}
|
|
1328
|
+
return new AuraPrefetchStorage(auraStorage, inMemoryStorage);
|
|
1248
1329
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1330
|
+
class AuraPrefetchStorage {
|
|
1331
|
+
constructor(auraStorage, inMemoryStorage) {
|
|
1332
|
+
this.auraStorage = auraStorage;
|
|
1333
|
+
this.inMemoryStorage = inMemoryStorage;
|
|
1334
|
+
/**
|
|
1335
|
+
* Because of aura is an event loop hog and we therefore need to minimize asynchronicity in LEX,
|
|
1336
|
+
* we need need to preload predictions and treat read operations sync. Not making it sync, will cause
|
|
1337
|
+
* some request to be sent the network when they could be dedupe against those from the predictions.
|
|
1338
|
+
*
|
|
1339
|
+
* Drawbacks of this approach:
|
|
1340
|
+
* 1. Loading all of aura storage into memory and then updating it based on changes to that in memory
|
|
1341
|
+
* representation means that updates to predictions in aura storage across different tabs will result
|
|
1342
|
+
* in overwrites, not graceful merges of predictions.
|
|
1343
|
+
* 2. If whoever is consuming this tries to get and run predictions before this is done loading,
|
|
1344
|
+
* then they will (potentially incorrectly) think that we don't have any predictions.
|
|
1345
|
+
*/
|
|
1346
|
+
auraStorage.getAll().then((results) => {
|
|
1347
|
+
ObjectKeys(results).forEach((key) => this.inMemoryStorage.set(key, results[key]));
|
|
1348
|
+
});
|
|
1260
1349
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
* Create a new instrumentation cache stats and return it.
|
|
1268
|
-
*
|
|
1269
|
-
* @param name The cache logger name.
|
|
1270
|
-
*/
|
|
1271
|
-
function registerLdsCacheStats(name) {
|
|
1272
|
-
return registerCacheStats(`${NAMESPACE}:${name}`);
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Add or overwrite hooks that require aura implementations
|
|
1276
|
-
*/
|
|
1277
|
-
function setAuraInstrumentationHooks() {
|
|
1278
|
-
instrument({
|
|
1279
|
-
recordConflictsResolved: (serverRequestCount) => {
|
|
1280
|
-
// Ignore 0 values which can originate from ADS bridge
|
|
1281
|
-
if (serverRequestCount > 0) {
|
|
1282
|
-
updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
|
|
1283
|
-
}
|
|
1284
|
-
},
|
|
1285
|
-
nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
|
|
1286
|
-
const metricName = `merge-null-dv-count.${fieldType}`;
|
|
1287
|
-
if (fieldType === 'scalar') {
|
|
1288
|
-
incrementCounterMetric(`${metricName}.${areValuesEqual}`);
|
|
1289
|
-
}
|
|
1290
|
-
else {
|
|
1291
|
-
incrementCounterMetric(metricName);
|
|
1350
|
+
set(key, value) {
|
|
1351
|
+
const inMemoryResult = this.inMemoryStorage.set(key, value);
|
|
1352
|
+
this.auraStorage.set(key, value).catch((error) => {
|
|
1353
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1354
|
+
// eslint-disable-next-line no-console
|
|
1355
|
+
console.error('Error save LDS prediction: ', error);
|
|
1292
1356
|
}
|
|
1293
|
-
},
|
|
1294
|
-
getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
|
|
1295
|
-
getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
|
|
1296
|
-
notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
|
|
1297
|
-
notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
|
|
1298
|
-
recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
|
|
1299
|
-
weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
|
|
1300
|
-
getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
|
|
1301
|
-
});
|
|
1302
|
-
withRegistration('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
|
|
1303
|
-
instrument$1({
|
|
1304
|
-
logCrud: logCRUDLightningInteraction,
|
|
1305
|
-
networkResponse: incrementRequestResponseCount,
|
|
1306
|
-
});
|
|
1307
|
-
instrument$2({
|
|
1308
|
-
refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
|
|
1309
|
-
instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
|
|
1310
|
-
});
|
|
1311
|
-
instrument$3({
|
|
1312
|
-
timerMetricAddDuration: updateTimerMetric,
|
|
1313
|
-
});
|
|
1314
|
-
// Our getRecord through aggregate-ui CRUD logging has moved
|
|
1315
|
-
// to lds-network-adapter. We still need to respect the
|
|
1316
|
-
// orgs environment setting
|
|
1317
|
-
if (forceRecordTransactionsDisabled === false) {
|
|
1318
|
-
ldsNetworkAdapterInstrument({
|
|
1319
|
-
getRecordAggregateResolve: (cb) => {
|
|
1320
|
-
const { recordId, apiName } = cb();
|
|
1321
|
-
logCRUDLightningInteraction('read', {
|
|
1322
|
-
recordId,
|
|
1323
|
-
recordType: apiName,
|
|
1324
|
-
state: 'SUCCESS',
|
|
1325
|
-
});
|
|
1326
|
-
},
|
|
1327
|
-
getRecordAggregateReject: (cb) => {
|
|
1328
|
-
const recordId = cb();
|
|
1329
|
-
logCRUDLightningInteraction('read', {
|
|
1330
|
-
recordId,
|
|
1331
|
-
state: 'ERROR',
|
|
1332
|
-
});
|
|
1333
|
-
},
|
|
1334
1357
|
});
|
|
1358
|
+
return inMemoryResult;
|
|
1359
|
+
}
|
|
1360
|
+
get(key) {
|
|
1361
|
+
// we never read from the AuraStorage, except in construction.
|
|
1362
|
+
return this.inMemoryStorage.get(key);
|
|
1335
1363
|
}
|
|
1336
|
-
withRegistration('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
|
|
1337
|
-
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
|
|
1340
|
-
*
|
|
1341
|
-
* @param luvio The Luvio instance to instrument.
|
|
1342
|
-
* @param store The InMemoryStore to instrument.
|
|
1343
|
-
*/
|
|
1344
|
-
function setupInstrumentation(luvio, store) {
|
|
1345
|
-
setupInstrumentation$1(luvio, store);
|
|
1346
|
-
setAuraInstrumentationHooks();
|
|
1347
|
-
}
|
|
1348
|
-
/**
|
|
1349
|
-
* Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
|
|
1350
|
-
* disable all crud operations if it is on.
|
|
1351
|
-
* @param eventSource - Source of the logging event.
|
|
1352
|
-
* @param attributes - Free form object of attributes to log.
|
|
1353
|
-
*/
|
|
1354
|
-
function logCRUDLightningInteraction(eventSource, attributes) {
|
|
1355
|
-
interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
|
|
1356
1364
|
}
|
|
1357
|
-
const instrumentation = new Instrumentation();
|
|
1358
1365
|
|
|
1359
1366
|
class NoComposedAdapterTypeError extends TypeError {
|
|
1360
1367
|
constructor(message, resourceRequest) {
|
|
@@ -1593,7 +1600,7 @@ function buildPredictorForContext(context) {
|
|
|
1593
1600
|
});
|
|
1594
1601
|
},
|
|
1595
1602
|
runPredictions() {
|
|
1596
|
-
return __lexPrefetcher.predict();
|
|
1603
|
+
return executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_PREDICT, (_act) => __lexPrefetcher.predict(), PDL_EXECUTE_ASYNC_OPTIONS);
|
|
1597
1604
|
},
|
|
1598
1605
|
};
|
|
1599
1606
|
}
|
|
@@ -1623,4 +1630,4 @@ function ldsEngineCreator() {
|
|
|
1623
1630
|
}
|
|
1624
1631
|
|
|
1625
1632
|
export { buildPredictorForContext, ldsEngineCreator as default, initializeLDS, predictiveLoadPage };
|
|
1626
|
-
// version: 1.266.0-
|
|
1633
|
+
// version: 1.266.0-dev22-06d4657e7
|