@salesforce/lds-runtime-aura 1.266.0-dev2 → 1.266.0-dev21

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