@salesforce/lds-runtime-aura 1.278.0 → 1.280.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  * For full license text, see the LICENSE.txt file
5
5
  */
6
6
 
7
- /* *******************************************************************************************
7
+ /*
8
8
  * ATTENTION!
9
9
  * THIS IS A GENERATED FILE FROM https://github.com/salesforce-experience-platform-emu/lds-lightning-platform
10
10
  * If you would like to contribute to LDS, please follow the steps outlined in the git repo.
@@ -12,19 +12,363 @@
12
12
  * *******************************************************************************************
13
13
  */
14
14
  /* proxy-compat-disable */
15
- import { HttpStatusCode, InMemoryStore, Environment, Luvio, InMemoryStoreQueryEvaluator } from 'force/luvioEngine';
15
+ import { HttpStatusCode, InMemoryStore as InMemoryStore$1, 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, getRelatedListsActionsAdapterFactory, getRelatedListInfoBatchAdapterFactory, getRelatedListRecordsAdapterFactory, getRelatedListRecordsBatchAdapterFactory, 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, getRelatedListsActionsAdapterFactory, getRelatedListInfoBatchAdapterFactory, getRelatedListRecordsBatchAdapterFactory, getRelatedListRecordsAdapterFactory, configuration, InMemoryRecordRepresentationQueryEvaluator, UiApiNamespace, RecordRepresentationRepresentationType, registerPrefetcher } from 'force/ldsAdaptersUiapi';
19
+ import oneStoreEnabled from '@salesforce/gate/lds.oneStoreEnabled.ltng';
20
+ import { executeGlobalControllerRawResponse } from 'aura';
21
21
  import { REFRESH_ADAPTER_EVENT, ADAPTER_UNFULFILLED_ERROR, instrument as instrument$2 } from 'force/ldsBindings';
22
22
  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';
23
+ 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';
24
24
  import auraNetworkAdapter, { instrument as instrument$1, forceRecordTransactionsDisabled, ldsNetworkAdapterInstrument, dispatchAuraAction, defaultActionConfig } from 'force/ldsNetwork';
25
25
  import { instrument as instrument$3 } from 'force/adsBridge';
26
+ import { withRegistration as withRegistration$1, register, setDefaultLuvio } from 'force/ldsEngine';
27
+ import { createStorage, clearStorages } from 'force/ldsStorage';
26
28
  import { buildJwtNetworkAdapter } from 'force/ldsNetworkFetchWithJwt';
27
29
 
30
+ /**
31
+ * Copyright (c) 2022, Salesforce, Inc.,
32
+ * All rights reserved.
33
+ * For full license text, see the LICENSE.txt file
34
+ */
35
+
36
+ const { hasOwnProperty } = Object.prototype;
37
+
38
+ function resolvedPromiseLike(result) {
39
+ // Don't nest anything promise like
40
+ if (isPromiseLike(result)) {
41
+ return result.then((nextResult) => nextResult);
42
+ }
43
+ return {
44
+ then: (onFulfilled, _onRejected) => {
45
+ if (onFulfilled) {
46
+ try {
47
+ return resolvedPromiseLike(onFulfilled(result));
48
+ }
49
+ catch (e) {
50
+ return rejectedPromiseLike(e);
51
+ }
52
+ }
53
+ // assume TResult1 == Result and just pass result down the chain
54
+ return resolvedPromiseLike(result);
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Returns a PromiseLike object that rejects with the specified reason.
60
+ *
61
+ * @param reason rejection value
62
+ * @returns PromiseLike that rejects with reason
63
+ */
64
+ function rejectedPromiseLike(reason) {
65
+ if (isPromiseLike(reason)) {
66
+ return reason.then((nextResult) => nextResult);
67
+ }
68
+ return {
69
+ then: (_onFulfilled, onRejected) => {
70
+ if (onRejected) {
71
+ try {
72
+ return resolvedPromiseLike(onRejected(reason));
73
+ }
74
+ catch (e) {
75
+ return rejectedPromiseLike(e);
76
+ }
77
+ }
78
+ // assume TResult2 == Result and just pass rejection down the chain
79
+ return rejectedPromiseLike(reason);
80
+ },
81
+ };
82
+ }
83
+ function isPromiseLike(value) {
84
+ return (value instanceof Promise ||
85
+ (typeof value === 'object' &&
86
+ value !== null &&
87
+ hasOwnProperty.call(value, 'then') &&
88
+ typeof value.then === 'function'));
89
+ }
90
+
91
+ /**
92
+ * A collection of keys, in no particular order.
93
+ */
94
+ class KeySetImpl {
95
+ constructor(initialKeys) {
96
+ // TODO - probably better to use Set<Key>
97
+ this.data = {};
98
+ this.lengthInternal = 0;
99
+ if (initialKeys) {
100
+ initialKeys.forEach((key) => {
101
+ this.add(key);
102
+ });
103
+ }
104
+ }
105
+ add(key) {
106
+ this.data[key] = true;
107
+ // TODO - need to account for adding a key that was already in the set
108
+ this.lengthInternal++;
109
+ }
110
+ contains(key) {
111
+ return this.data[key] === true;
112
+ }
113
+ elements() {
114
+ return Object.keys(this.data);
115
+ }
116
+ get length() {
117
+ return this.lengthInternal;
118
+ }
119
+ overlaps(other) {
120
+ const otherKeys = other.elements();
121
+ for (let j = 0; j < otherKeys.length; ++j) {
122
+ if (this.contains(otherKeys[j])) {
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ difference(other) {
129
+ const value = new KeySetImpl();
130
+ this.elements().forEach((key) => {
131
+ if (!other.contains(key)) {
132
+ value.add(key);
133
+ }
134
+ });
135
+ return value;
136
+ }
137
+ isSubsetOf(other) {
138
+ return this.difference(other).length === 0;
139
+ }
140
+ equals(other) {
141
+ return this.length === other.length && this.elements().every((key) => other.contains(key));
142
+ }
143
+ toString() {
144
+ return `<<${JSON.stringify(this.elements())}>>`;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * A simple in-memory implementation of the Store interface.
150
+ *
151
+ * Exported for testing purposes only.
152
+ */
153
+ class InMemoryStore {
154
+ constructor() {
155
+ this.data = {};
156
+ }
157
+ // TODO - only intended for use in tests. We should refactor this into a subclass &
158
+ // add to a test util library.
159
+ clear() {
160
+ this.data = {};
161
+ }
162
+ delete(key, _options) {
163
+ delete this.data[key];
164
+ }
165
+ get(key, _options) {
166
+ return this.data[key];
167
+ }
168
+ set(key, value, _options) {
169
+ this.data[key] = value;
170
+ }
171
+ length() {
172
+ return this.keys().length;
173
+ }
174
+ keys() {
175
+ return new KeySetImpl(Object.keys(this.data));
176
+ }
177
+ toKeySet(keys) {
178
+ return new KeySetImpl(keys);
179
+ }
180
+ }
181
+ /**
182
+ * Constructs an in-memory implementation of StoreService.
183
+ *
184
+ * @returns in-memory implementation of StoreService
185
+ */
186
+ function buildInMemoryStoreService() {
187
+ return {
188
+ store: new InMemoryStore(),
189
+ };
190
+ }
191
+ /**
192
+ * A simple in-memory implementation of the MetadataRepository interface.
193
+ *
194
+ * Exported for testing purposes only.
195
+ */
196
+ class InMemoryMetadataRepository {
197
+ constructor() {
198
+ this.data = {};
199
+ }
200
+ // TODO - only intended for use in tests. We should refactor this into a subclass &
201
+ // add to a test util library.
202
+ clear() {
203
+ this.data = {};
204
+ }
205
+ delete(key, _options) {
206
+ delete this.data[key];
207
+ }
208
+ get(key, _options) {
209
+ return this.data[key];
210
+ }
211
+ set(key, value, _options) {
212
+ this.data[key] = value;
213
+ }
214
+ setPartial(key, value, _options) {
215
+ const metadata = this.get(key);
216
+ if (metadata === undefined) {
217
+ throw new Error(`Metadata for key "${key}" not found`);
218
+ }
219
+ this.data[key] = { ...metadata, ...value };
220
+ }
221
+ expire(key, options) {
222
+ const { expirationTime = Date.now() } = options || {};
223
+ this.setPartial(key, { expirationTime });
224
+ }
225
+ }
226
+ /**
227
+ * Constructs an in-memory implementation of MetadataRepositoryService.
228
+ *
229
+ * @returns in-memory implementation of MetadataRepositoryService
230
+ */
231
+ function buildInMemoryMetadataRepositoryService() {
232
+ return {
233
+ metadataRepository: new InMemoryMetadataRepository(),
234
+ };
235
+ }
236
+
237
+ /**
238
+ * A simple implementation of KeyKeySubscriptionService.
239
+ */
240
+ class DefaultKeySubscriptionService {
241
+ constructor() {
242
+ this.nextId = 1;
243
+ this.subscriptions = [];
244
+ }
245
+ subscribe(options) {
246
+ const subscriptionId = this.nextId++;
247
+ const { subscription, callback } = options;
248
+ this.subscriptions.push({ subscriptionId, keys: subscription, callback });
249
+ return () => {
250
+ this.subscriptions = this.subscriptions.filter((subscription) => subscription.subscriptionId !== subscriptionId);
251
+ };
252
+ }
253
+ publish(keys, _options) {
254
+ const subscriptions = this.subscriptions.slice();
255
+ for (let i = 0; i < subscriptions.length; ++i) {
256
+ const { keys: subscriptionKeys, callback } = subscriptions[i];
257
+ if (keys.overlaps(subscriptionKeys)) {
258
+ callback();
259
+ }
260
+ }
261
+ return resolvedPromiseLike(undefined);
262
+ }
263
+ }
264
+ /**
265
+ * Constructs a default KeySubscriptionService
266
+ *
267
+ * @returns default KeySubscriptionService
268
+ */
269
+ function buildDefaultKeySubscriptionService() {
270
+ return {
271
+ keySubscription: new DefaultKeySubscriptionService(),
272
+ };
273
+ }
274
+
275
+ class DefaultTypeNotFoundError extends Error {
276
+ }
277
+ class DefaultTypeRegistry {
278
+ constructor() {
279
+ this.registry = {};
280
+ this.TypeNotFoundError = DefaultTypeNotFoundError;
281
+ }
282
+ register(type, _options) {
283
+ if (!this.registry[type.namespace]) {
284
+ this.registry[type.namespace] = {};
285
+ }
286
+ this.registry[type.namespace][type.typeName] = type;
287
+ }
288
+ get(namespace, typeName, _options) {
289
+ const registryNamespace = this.registry[namespace];
290
+ if (!registryNamespace) {
291
+ throw new DefaultTypeNotFoundError(`namespace ${namespace} not found`);
292
+ }
293
+ const type = registryNamespace[typeName];
294
+ if (!type) {
295
+ throw new DefaultTypeNotFoundError(`type ${typeName} not found in namespace ${namespace}`);
296
+ }
297
+ return type;
298
+ }
299
+ }
300
+ /**
301
+ * Constructs an in-memory implementation of StoreService.
302
+ *
303
+ * @returns in-memory implementation of StoreService
304
+ */
305
+ function buildDefaultTypeRegistryService() {
306
+ return {
307
+ typeRegistry: new DefaultTypeRegistry(),
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Copyright (c) 2022, Salesforce, Inc.,
313
+ * All rights reserved.
314
+ * For full license text, see the LICENSE.txt file
315
+ */
316
+
317
+ /**
318
+ * Registrations that have already occurred.
319
+ *
320
+ * Note that Registrations are maintained as a list rather than a map to allow
321
+ * the same id to be registered multiple times with potentially different
322
+ * data.
323
+ */
324
+ const registrations = [];
325
+ /**
326
+ * Invokes callback for each Registration, both past & future. That is, callback
327
+ * will be invoked exactly as many times as register() is called.
328
+ *
329
+ * Note that Registration ids are not guaranteed to be unique. The meaning of
330
+ * multiple Registrations with the same id is determined by the caller(s) of
331
+ * register().
332
+ */
333
+ function forEachRegistration(callback) {
334
+ registrations.forEach((r) => callback(r));
335
+ }
336
+ /**
337
+ * Invokes callback when the specified id is registered.
338
+ *
339
+ * Note that callback may be invoked:
340
+ *
341
+ * - multiple times if multiple calls to register() specify the id
342
+ * - never if the specified id is never registered
343
+ */
344
+ function withRegistration(id, callback) {
345
+ forEachRegistration((r) => {
346
+ if (r.id === id) {
347
+ callback(r);
348
+ }
349
+ });
350
+ }
351
+
352
+ /**
353
+ * Copyright (c) 2022, Salesforce, Inc.,
354
+ * All rights reserved.
355
+ * For full license text, see the LICENSE.txt file
356
+ */
357
+
358
+ /**
359
+ * Indicates if a given instance of a service satisfies a request. Note that this function
360
+ * assumes the version numbers are valid strings.
361
+ *
362
+ * @param provided ServiceVersion of the service instance to be provided
363
+ * @param requested ServiceVersion requested
364
+ * @returns true if the service instance to be provided satisfies the request
365
+ */
366
+ function satisfies(provided, requested) {
367
+ const providedN = provided.split('.').map((s) => parseInt(s));
368
+ const requestedN = requested.split('.').map((s) => parseInt(s));
369
+ return providedN[0] === requestedN[0] && providedN[1] >= requestedN[1];
370
+ }
371
+
28
372
  class PredictivePrefetchPage {
29
373
  constructor(context) {
30
374
  this.context = context;
@@ -133,122 +477,88 @@ class RecordHomePage extends PredictivePrefetchPage {
133
477
  }
134
478
  }
135
479
 
136
- class ApplicationPredictivePrefetcher {
137
- constructor(context, repository, requestRunner) {
138
- this.repository = repository;
139
- this.requestRunner = requestRunner;
140
- this.isRecording = false;
141
- this.queuedPredictionRequests = [];
142
- this._context = context;
143
- this.page = this.getPage();
144
- }
145
- set context(value) {
146
- this._context = value;
147
- this.page = this.getPage();
480
+ /**
481
+ * Observability / Critical Availability Program (230+)
482
+ *
483
+ * This file is intended to be used as a consolidated place for all definitions, functions,
484
+ * and helpers related to "M1"[1].
485
+ *
486
+ * Below are the R.E.A.D.S. metrics for the Lightning Data Service, defined here[2].
487
+ *
488
+ * [1] Search "[M1] Lightning Data Service Design Spike" in Quip
489
+ * [2] Search "Lightning Data Service R.E.A.D.S. Metrics" in Quip
490
+ */
491
+ const OBSERVABILITY_NAMESPACE = 'LIGHTNING.lds.service';
492
+ const ADAPTER_INVOCATION_COUNT_METRIC_NAME = 'request';
493
+ const ADAPTER_ERROR_COUNT_METRIC_NAME = 'error';
494
+ const NETWORK_ADAPTER_RESPONSE_METRIC_NAME = 'network-response';
495
+ /**
496
+ * W-8379680
497
+ * Counter for number of getApex requests.
498
+ */
499
+ const GET_APEX_REQUEST_COUNT = {
500
+ get() {
501
+ return {
502
+ owner: OBSERVABILITY_NAMESPACE,
503
+ name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
504
+ };
505
+ },
506
+ };
507
+ /**
508
+ * W-8828410
509
+ * Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
510
+ */
511
+ const TOTAL_ADAPTER_ERROR_COUNT = {
512
+ get() {
513
+ return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
514
+ },
515
+ };
516
+ /**
517
+ * W-8828410
518
+ * Counter for the number of invocations made into LDS by a wire adapter.
519
+ */
520
+ const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
521
+ get() {
522
+ return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
523
+ },
524
+ };
525
+
526
+ const { create, keys } = Object;
527
+ const { isArray } = Array;
528
+ const { stringify } = JSON;
529
+
530
+ /**
531
+ * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
532
+ * This is needed because insertion order for JSON.stringify(object) affects output:
533
+ * JSON.stringify({a: 1, b: 2})
534
+ * "{"a":1,"b":2}"
535
+ * JSON.stringify({b: 2, a: 1})
536
+ * "{"b":2,"a":1}"
537
+ * Modified from the apex implementation to sort arrays non-destructively.
538
+ * @param data Data to be JSON-stringified.
539
+ * @returns JSON.stringified value with consistent ordering of keys.
540
+ */
541
+ function stableJSONStringify$1(node) {
542
+ // This is for Date values.
543
+ if (node && node.toJSON && typeof node.toJSON === 'function') {
544
+ // eslint-disable-next-line no-param-reassign
545
+ node = node.toJSON();
148
546
  }
149
- get context() {
150
- return this._context;
547
+ if (node === undefined) {
548
+ return;
151
549
  }
152
- async stopRecording() {
153
- this.isRecording = false;
154
- await this.repository.flushRequestsToStorage();
550
+ if (typeof node === 'number') {
551
+ return isFinite(node) ? '' + node : 'null';
155
552
  }
156
- startRecording() {
157
- this.isRecording = true;
158
- this.repository.clearRequestBuffer();
159
- }
160
- saveRequest(request) {
161
- if (!this.isRecording) {
162
- return Promise.resolve();
163
- }
164
- const { request: requestToSave, context } = this.page.buildSaveRequestData(request);
165
- // No need to diferentiate from predictions requests because these
166
- // are made from the adapters factory, which are not prediction aware.
167
- return this.repository.saveRequest(context, requestToSave);
168
- }
169
- async predict() {
170
- const exactPageRequests = (await this.repository.getPageRequests(this.context)) || [];
171
- const similarPageRequests = await this.getSimilarPageRequests();
172
- const alwaysRequests = this.page.getAlwaysRunRequests();
173
- const predictedRequests = [
174
- ...alwaysRequests,
175
- ...this.requestRunner.reduceRequests([
176
- ...exactPageRequests,
177
- ...similarPageRequests,
178
- ...this.page.getAlwaysRunRequests(),
179
- ]),
180
- ];
181
- this.queuedPredictionRequests.push(...predictedRequests);
182
- return Promise.all(predictedRequests.map((request) => this.requestRunner.runRequest(request))).then();
183
- }
184
- hasPredictions() {
185
- const exactPageRequests = this.repository.getPageRequests(this.context) || [];
186
- const similarPageRequests = this.page.similarContext !== undefined
187
- ? this.repository.getPageRequests(this.page.similarContext)
188
- : [];
189
- return exactPageRequests.length > 0 || similarPageRequests.length > 0;
190
- }
191
- getSimilarPageRequests() {
192
- let resolvedSimilarPageRequests = [];
193
- if (this.page.similarContext !== undefined) {
194
- const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
195
- if (similarPageRequests !== undefined) {
196
- resolvedSimilarPageRequests = similarPageRequests.map((request) => this.page.resolveSimilarRequest(request));
197
- }
198
- }
199
- return resolvedSimilarPageRequests;
200
- }
201
- }
202
-
203
- class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
204
- constructor(context, repository, requestRunner,
205
- // These strategies need to be in sync with the "predictiveDataLoadCapable" list
206
- // from scripts/lds-uiapi-plugin.js
207
- requestStrategies) {
208
- super(context, repository, requestRunner);
209
- this.requestStrategies = requestStrategies;
210
- this.page = this.getPage();
211
- }
212
- getPage() {
213
- if (RecordHomePage.handlesContext(this.context)) {
214
- return new RecordHomePage(this.context, this.requestStrategies);
215
- }
216
- return new LexDefaultPage(this.context);
217
- }
218
- }
219
-
220
- // Copy-pasted from adapter-utils. This util should be extracted from generated code and imported in prefetch repository.
221
- const { keys: ObjectKeys$2 } = Object;
222
- const { stringify: JSONStringify } = JSON;
223
- const { isArray: ArrayIsArray$1 } = Array;
224
- /**
225
- * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
226
- * This is needed because insertion order for JSON.stringify(object) affects output:
227
- * JSON.stringify({a: 1, b: 2})
228
- * "{"a":1,"b":2}"
229
- * JSON.stringify({b: 2, a: 1})
230
- * "{"b":2,"a":1}"
231
- * @param data Data to be JSON-stringified.
232
- * @returns JSON.stringified value with consistent ordering of keys.
233
- */
234
- function stableJSONStringify$1(node) {
235
- // This is for Date values.
236
- if (node && node.toJSON && typeof node.toJSON === 'function') {
237
- // eslint-disable-next-line no-param-reassign
238
- node = node.toJSON();
239
- }
240
- if (node === undefined) {
241
- return;
242
- }
243
- if (typeof node === 'number') {
244
- return isFinite(node) ? '' + node : 'null';
245
- }
246
- if (typeof node !== 'object') {
247
- return JSONStringify(node);
553
+ if (typeof node !== 'object') {
554
+ return stringify(node);
248
555
  }
249
556
  let i;
250
557
  let out;
251
- if (ArrayIsArray$1(node)) {
558
+ if (isArray(node)) {
559
+ // copy any array before sorting so we don't mutate the object.
560
+ // eslint-disable-next-line no-param-reassign
561
+ node = node.slice(0).sort();
252
562
  out = '[';
253
563
  for (i = 0; i < node.length; i++) {
254
564
  if (i) {
@@ -261,10 +571,10 @@ function stableJSONStringify$1(node) {
261
571
  if (node === null) {
262
572
  return 'null';
263
573
  }
264
- const keys = ObjectKeys$2(node).sort();
574
+ const keys$1 = keys(node).sort();
265
575
  out = '';
266
- for (i = 0; i < keys.length; i++) {
267
- const key = keys[i];
576
+ for (i = 0; i < keys$1.length; i++) {
577
+ const key = keys$1[i];
268
578
  const value = stableJSONStringify$1(node[key]);
269
579
  if (!value) {
270
580
  continue;
@@ -272,400 +582,833 @@ function stableJSONStringify$1(node) {
272
582
  if (out) {
273
583
  out += ',';
274
584
  }
275
- out += JSONStringify(key) + ':' + value;
585
+ out += stringify(key) + ':' + value;
276
586
  }
277
587
  return '{' + out + '}';
278
588
  }
279
- function isObject(obj) {
280
- return obj !== null && typeof obj === 'object';
281
- }
282
- function deepEquals(objA, objB) {
283
- if (objA === objB)
284
- return true;
285
- if (objA instanceof Date && objB instanceof Date)
286
- return objA.getTime() === objB.getTime();
287
- // If one of them is not an object, they are not deeply equal
288
- if (!isObject(objA) || !isObject(objB))
289
- return false;
290
- // Filter out keys set as undefined, we can compare undefined as equals.
291
- const keysA = ObjectKeys$2(objA).filter((key) => objA[key] !== undefined);
292
- const keysB = ObjectKeys$2(objB).filter((key) => objB[key] !== undefined);
293
- // If the objects do not have the same set of keys, they are not deeply equal
294
- if (keysA.length !== keysB.length)
295
- return false;
296
- for (const key of keysA) {
297
- const valA = objA[key];
298
- const valB = objB[key];
299
- const areObjects = isObject(valA) && isObject(valB);
300
- // If both values are objects, recursively compare them
301
- if (areObjects && !deepEquals(valA, valB))
302
- return false;
303
- // If only one value is an object or if the values are not strictly equal, they are not deeply equal
304
- if (!areObjects && valA !== valB)
305
- return false;
306
- }
307
- return true;
589
+ function isPromise(value) {
590
+ // check for Thenable due to test frameworks using custom Promise impls
591
+ return value !== null && value.then !== undefined;
308
592
  }
309
593
 
310
- class PrefetchRepository {
311
- constructor(storage) {
312
- this.storage = storage;
313
- this.requestBuffer = new Map();
314
- }
315
- clearRequestBuffer() {
316
- this.requestBuffer.clear();
317
- }
318
- async flushRequestsToStorage() {
319
- const setPromises = [];
320
- for (const [id, batch] of this.requestBuffer) {
321
- const rawPage = this.storage.get(id);
322
- const page = rawPage === undefined ? { id, requests: [] } : rawPage;
323
- batch.forEach(({ request, requestTime }) => {
324
- let existingRequestEntry = page.requests.find(({ request: storedRequest }) => deepEquals(storedRequest, request));
325
- if (existingRequestEntry === undefined) {
326
- existingRequestEntry = { request, requestMetadata: { requestTimes: [] } };
327
- page.requests.push(existingRequestEntry);
594
+ const APEX_ADAPTER_NAME = 'getApex';
595
+ const NORMALIZED_APEX_ADAPTER_NAME = `Apex.${APEX_ADAPTER_NAME}`;
596
+ const REFRESH_APEX_KEY = 'refreshApex';
597
+ const REFRESH_UIAPI_KEY = 'refreshUiApi';
598
+ const SUPPORTED_KEY = 'refreshSupported';
599
+ const UNSUPPORTED_KEY = 'refreshUnsupported';
600
+ const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
601
+ const REFRESH_EVENTTYPE = 'system';
602
+ const REFRESH_PAYLOAD_TARGET = 'adapters';
603
+ const REFRESH_PAYLOAD_SCOPE = 'lds';
604
+ const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
605
+ const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
606
+ const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
607
+ const NAMESPACE = 'lds';
608
+ const NETWORK_TRANSACTION_NAME = 'lds-network';
609
+ const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
610
+ // Aggregate Cache Stats and Metrics for all getApex invocations
611
+ const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
612
+ const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
613
+ // Observability (READS)
614
+ const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
615
+ const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
616
+ const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
617
+ class Instrumentation {
618
+ constructor() {
619
+ this.adapterUnfulfilledErrorCounters = {};
620
+ this.recordApiNameChangeCounters = {};
621
+ this.refreshAdapterEvents = {};
622
+ this.refreshApiCallEventStats = {
623
+ [REFRESH_APEX_KEY]: 0,
624
+ [REFRESH_UIAPI_KEY]: 0,
625
+ [SUPPORTED_KEY]: 0,
626
+ [UNSUPPORTED_KEY]: 0,
627
+ };
628
+ this.lastRefreshApiCall = null;
629
+ this.weakEtagZeroEvents = {};
630
+ this.adapterCacheMisses = new LRUCache(250);
631
+ if (typeof window !== 'undefined' && window.addEventListener) {
632
+ window.addEventListener('beforeunload', () => {
633
+ if (keys(this.weakEtagZeroEvents).length > 0) {
634
+ perfStart(NETWORK_TRANSACTION_NAME);
635
+ perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
328
636
  }
329
- existingRequestEntry.requestMetadata.requestTimes.push(requestTime);
330
637
  });
331
- setPromises.push(this.storage.set(id, page));
332
- }
333
- this.clearRequestBuffer();
334
- await Promise.all(setPromises);
335
- }
336
- getKeyId(key) {
337
- return stableJSONStringify$1(key);
338
- }
339
- async saveRequest(key, request) {
340
- const identifier = this.getKeyId(key);
341
- const batchForKey = this.requestBuffer.get(identifier) || [];
342
- batchForKey.push({
343
- request,
344
- requestTime: Date.now(),
345
- });
346
- this.requestBuffer.set(identifier, batchForKey);
347
- }
348
- getPage(key) {
349
- const identifier = stableJSONStringify$1(key);
350
- return this.storage.get(identifier);
351
- }
352
- getPageRequests(key) {
353
- const page = this.getPage(key);
354
- if (page === undefined) {
355
- return [];
356
638
  }
357
- return page.requests.map((requestEntry) => requestEntry.request);
358
- }
359
- }
360
-
361
- class RequestStrategy {
362
- transformForSave(request) {
363
- return request;
364
- }
365
- reduce(requests) {
366
- return requests;
367
- }
368
- }
369
-
370
- class LuvioAdapterRequestStrategy extends RequestStrategy {
371
- /**
372
- * Perform any transformations required to prepare the request for saving.
373
- *
374
- * e.g. If the request is for a record, we move all fields in the fields array
375
- * into the optionalFields array
376
- *
377
- * @param request - The request to transform
378
- * @returns
379
- */
380
- transformForSave(request) {
381
- return request;
382
- }
383
- /**
384
- * Filter requests to only those that are for this strategy.
385
- *
386
- * @param unfilteredRequests array of requests to filter
387
- * @returns
388
- */
389
- filterRequests(unfilteredRequests) {
390
- return unfilteredRequests.filter((request) => request.adapterName === this.adapterName);
639
+ registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
391
640
  }
392
641
  /**
393
- * Reduce requests by combining those based on a strategies implementations
394
- * of canCombine and combineRequests.
395
- * @param unfilteredRequests array of requests to filter
396
- * @returns
642
+ * Instruments an existing adapter to log argus metrics and cache stats.
643
+ * @param adapter The adapter function.
644
+ * @param metadata The adapter metadata.
645
+ * @param wireConfigKeyFn Optional function to transform wire configs to a unique key.
646
+ * @returns The wrapped adapter.
397
647
  */
398
- reduce(unfilteredRequests) {
399
- const requests = this.filterRequests(unfilteredRequests);
400
- const visitedRequests = new Set();
401
- const reducedRequests = [];
402
- for (let i = 0, n = requests.length; i < n; i++) {
403
- const currentRequest = requests[i];
404
- if (!visitedRequests.has(currentRequest)) {
405
- const combinedRequest = { ...currentRequest };
406
- for (let j = i + 1; j < n; j++) {
407
- const hasNotBeenVisited = !visitedRequests.has(requests[j]);
408
- const canCombineConfigs = this.canCombine(combinedRequest.config, requests[j].config);
409
- if (hasNotBeenVisited && canCombineConfigs) {
410
- combinedRequest.config = this.combineRequests(combinedRequest.config, requests[j].config);
411
- visitedRequests.add(requests[j]);
412
- }
648
+ instrumentAdapter(adapter, metadata) {
649
+ // We are consolidating all apex adapter instrumentation calls under a single key
650
+ const { apiFamily, name, ttl } = metadata;
651
+ const adapterName = normalizeAdapterName(name, apiFamily);
652
+ const isGetApexAdapter = isApexAdapter(name);
653
+ const stats = isGetApexAdapter ? getApexCacheStats : registerLdsCacheStats(adapterName);
654
+ const ttlMissStats = isGetApexAdapter
655
+ ? getApexTtlCacheStats
656
+ : registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
657
+ /**
658
+ * W-8076905
659
+ * Dynamically generated metric. Simple counter for all requests made by this adapter.
660
+ */
661
+ const wireAdapterRequestMetric = isGetApexAdapter
662
+ ? getApexRequestCountMetric
663
+ : counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
664
+ const instrumentedAdapter = (config, requestContext) => {
665
+ // increment overall and adapter request metrics
666
+ wireAdapterRequestMetric.increment(1);
667
+ totalAdapterRequestSuccessMetric.increment(1);
668
+ // execute adapter logic
669
+ const result = adapter(config, requestContext);
670
+ // In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
671
+ // (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
672
+ //
673
+ // Note: we can't do a plain instanceof check for a promise here since the Promise may
674
+ // originate from another javascript realm (for example: in jest test). Instead we use a
675
+ // duck-typing approach by checking if the result has a then property.
676
+ //
677
+ // For adapters without persistent store:
678
+ // - total cache hit ratio:
679
+ // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
680
+ // For adapters with persistent store:
681
+ // - in-memory cache hit ratio:
682
+ // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
683
+ // - total cache hit ratio:
684
+ // ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
685
+ // if result === null then config is insufficient/invalid so do not log
686
+ if (isPromise(result)) {
687
+ stats.logMisses();
688
+ if (ttl !== undefined) {
689
+ this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
413
690
  }
414
- reducedRequests.push(combinedRequest);
415
- visitedRequests.add(currentRequest);
416
691
  }
417
- }
418
- return reducedRequests;
692
+ else if (result !== null) {
693
+ stats.logHits();
694
+ }
695
+ return result;
696
+ };
697
+ // Set the name property on the function for debugging purposes.
698
+ Object.defineProperty(instrumentedAdapter, 'name', {
699
+ value: name + '__instrumented',
700
+ });
701
+ return instrumentAdapter(instrumentedAdapter, metadata);
419
702
  }
420
703
  /**
421
- * Check if two requests can be combined.
422
- *
423
- * By default, requests are not combinable.
424
- * @param reqA config of request A
425
- * @param reqB config of request B
426
- * @returns
704
+ * 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.
705
+ * Backed by an LRU Cache implementation to prevent too many record entries from being stored in-memory.
706
+ * @param name The wire adapter name.
707
+ * @param config The config passed into wire adapter.
708
+ * @param ttlMissStats CacheStatsLogger to log misses out of TTL.
709
+ * @param currentCacheMissTimestamp Timestamp for when the request was made.
710
+ * @param ttl TTL for the wire adapter.
427
711
  */
428
- canCombine(_reqA, _reqB) {
429
- return false;
712
+ logAdapterCacheMissOutOfTtlDuration(name, config, ttlMissStats, currentCacheMissTimestamp, ttl) {
713
+ const configKey = `${name}:${stableJSONStringify$1(config)}`;
714
+ const existingCacheMissTimestamp = this.adapterCacheMisses.get(configKey);
715
+ this.adapterCacheMisses.set(configKey, currentCacheMissTimestamp);
716
+ if (existingCacheMissTimestamp !== undefined) {
717
+ const duration = currentCacheMissTimestamp - existingCacheMissTimestamp;
718
+ if (duration > ttl) {
719
+ ttlMissStats.logMisses();
720
+ }
721
+ }
430
722
  }
431
723
  /**
432
- * Takes two request configs and combines them into a single request config.
724
+ * Injected to LDS for Luvio specific instrumentation.
433
725
  *
434
- * @param reqA config of request A
435
- * @param reqB config of request B
436
- * @returns
726
+ * @param context The transaction context.
437
727
  */
438
- combineRequests(reqA, _reqB) {
439
- // By default, this should never be called since requests aren't combinable
440
- if (process.env.NODE_ENV !== 'production') {
441
- throw new Error('Not implemented');
728
+ instrumentLuvio(context) {
729
+ instrumentLuvio(context);
730
+ if (this.isRefreshAdapterEvent(context)) {
731
+ this.aggregateRefreshAdapterEvents(context);
442
732
  }
443
- return reqA;
733
+ else if (this.isAdapterUnfulfilledError(context)) {
734
+ this.incrementAdapterRequestErrorCount(context);
735
+ }
736
+ else ;
444
737
  }
445
738
  /**
446
- * Checks adapter config against request context to determine if the request is context dependent.
447
- *
448
- * By default, requests are not context dependent.
449
- * @param request
450
- * @returns
739
+ * Returns whether or not this is a RefreshAdapterEvent.
740
+ * @param context The transaction context.
741
+ * @returns Whether or not this is a RefreshAdapterEvent.
451
742
  */
452
- isContextDependent(_context, _request) {
453
- return false;
743
+ isRefreshAdapterEvent(context) {
744
+ return context[REFRESH_ADAPTER_EVENT] === true;
454
745
  }
455
746
  /**
456
- * Builds request for saving,
457
- * - transforming the request
458
- * - handling the cases where the request is context dependent (this is homework for the subclass)
459
- * @param _similarContext Context with at least one parameter as a wildcard '*'
460
- * @param context Exact context for a given page
461
- * @param request
462
- * @returns
747
+ * Returns whether or not this is an AdapterUnfulfilledError.
748
+ * @param context The transaction context.
749
+ * @returns Whether or not this is an AdapterUnfulfilledError.
463
750
  */
464
- buildSaveRequestData(_similarContext, context, request) {
465
- return {
466
- request: this.transformForSave(request),
467
- context,
468
- };
469
- }
470
- }
471
-
472
- function normalizeRecordIds$1(recordIds) {
473
- if (!Array.isArray(recordIds)) {
474
- return [recordIds];
475
- }
476
- return recordIds;
477
- }
478
- class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
479
- constructor() {
480
- super(...arguments);
481
- this.adapterName = 'getRecordAvatars';
482
- this.adapterFactory = getRecordAvatarsAdapterFactory;
751
+ isAdapterUnfulfilledError(context) {
752
+ return context[ADAPTER_UNFULFILLED_ERROR] === true;
483
753
  }
484
- buildConcreteRequest(similarRequest, context) {
485
- return {
486
- ...similarRequest,
487
- config: {
488
- ...similarRequest.config,
489
- recordIds: [context.recordId],
490
- },
491
- };
754
+ /**
755
+ * Specific instrumentation for getRecordNotifyChange.
756
+ * temporary implementation to match existing aura call for now
757
+ *
758
+ * @param uniqueWeakEtags whether weakEtags match or not
759
+ * @param error if dispatchResourceRequest fails for any reason
760
+ */
761
+ notifyChangeNetwork(uniqueWeakEtags, error) {
762
+ perfStart(NETWORK_TRANSACTION_NAME);
763
+ if (error === true) {
764
+ perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
765
+ }
766
+ else {
767
+ perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
768
+ }
492
769
  }
493
- buildSaveRequestData(similarContext, context, request) {
494
- if (this.isContextDependent(context, request)) {
495
- return {
496
- request: this.transformForSave({
497
- ...request,
498
- config: {
499
- ...request.config,
500
- recordIds: ['*'],
501
- },
502
- }),
503
- context: similarContext,
770
+ /**
771
+ * Parses and aggregates weakETagZero events to be sent in summarized log line.
772
+ * @param context The transaction context.
773
+ */
774
+ aggregateWeakETagEvents(incomingWeakEtagZero, existingWeakEtagZero, apiName) {
775
+ const key = 'weaketag-0-' + apiName;
776
+ if (this.weakEtagZeroEvents[key] === undefined) {
777
+ this.weakEtagZeroEvents[key] = {
778
+ [EXISTING_WEAKETAG_0_KEY]: 0,
779
+ [INCOMING_WEAKETAG_0_KEY]: 0,
504
780
  };
505
781
  }
506
- return {
507
- request: this.transformForSave(request),
508
- context,
509
- };
510
- }
511
- isContextDependent(context, request) {
512
- return (request.config.recordIds &&
513
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
514
- (request.config.recordIds.length === 1 &&
515
- request.config.recordIds[0] === context.recordId)));
782
+ if (existingWeakEtagZero) {
783
+ this.weakEtagZeroEvents[key][EXISTING_WEAKETAG_0_KEY] += 1;
784
+ }
785
+ if (incomingWeakEtagZero) {
786
+ this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
787
+ }
516
788
  }
517
- canCombine(reqA, reqB) {
518
- return reqA.formFactor === reqB.formFactor;
789
+ /**
790
+ * Aggregates refresh adapter events to be sent in summarized log line.
791
+ * - how many times refreshApex is called
792
+ * - how many times refresh from lightning/uiRecordApi is called
793
+ * - number of supported calls: refreshApex called on apex adapter
794
+ * - number of unsupported calls: refreshApex on non-apex adapter
795
+ * + any use of refresh from uiRecordApi module
796
+ * - count of refresh calls per adapter
797
+ * @param context The refresh adapter event.
798
+ */
799
+ aggregateRefreshAdapterEvents(context) {
800
+ // We are consolidating all apex adapter instrumentation calls under a single key
801
+ // Adding additional logging that getApex adapters can invoke? Read normalizeAdapterName ts-doc.
802
+ const adapterName = normalizeAdapterName(context.adapterName);
803
+ if (this.lastRefreshApiCall === REFRESH_APEX_KEY) {
804
+ if (isApexAdapter(adapterName)) {
805
+ this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
806
+ }
807
+ else {
808
+ this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
809
+ }
810
+ }
811
+ else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
812
+ this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
813
+ }
814
+ if (this.refreshAdapterEvents[adapterName] === undefined) {
815
+ this.refreshAdapterEvents[adapterName] = 0;
816
+ }
817
+ this.refreshAdapterEvents[adapterName] += 1;
818
+ this.lastRefreshApiCall = null;
519
819
  }
520
- combineRequests(reqA, reqB) {
521
- const combined = { ...reqA };
522
- combined.recordIds = Array.from(new Set([...normalizeRecordIds$1(reqA.recordIds), ...normalizeRecordIds$1(reqB.recordIds)]));
523
- return combined;
820
+ /**
821
+ * Increments call stat for incoming refresh api call, and sets the name
822
+ * to be used in {@link aggregateRefreshCalls}
823
+ * @param from The name of the refresh function called.
824
+ */
825
+ handleRefreshApiCall(apiName) {
826
+ this.refreshApiCallEventStats[apiName] += 1;
827
+ // set function call to be used with aggregateRefreshCalls
828
+ this.lastRefreshApiCall = apiName;
829
+ }
830
+ /**
831
+ * W-7302241
832
+ * Logs refresh call summary stats as a LightningInteraction.
833
+ */
834
+ logRefreshStats() {
835
+ if (keys(this.refreshAdapterEvents).length > 0) {
836
+ interaction(REFRESH_PAYLOAD_TARGET, REFRESH_PAYLOAD_SCOPE, this.refreshAdapterEvents, REFRESH_EVENTSOURCE, REFRESH_EVENTTYPE, this.refreshApiCallEventStats);
837
+ this.resetRefreshStats();
838
+ }
839
+ }
840
+ /**
841
+ * Resets the stat trackers for refresh call events.
842
+ */
843
+ resetRefreshStats() {
844
+ this.refreshAdapterEvents = {};
845
+ this.refreshApiCallEventStats = {
846
+ [REFRESH_APEX_KEY]: 0,
847
+ [REFRESH_UIAPI_KEY]: 0,
848
+ [SUPPORTED_KEY]: 0,
849
+ [UNSUPPORTED_KEY]: 0,
850
+ };
851
+ this.lastRefreshApiCall = null;
852
+ }
853
+ /**
854
+ * W-7801618
855
+ * Counter for occurrences where the incoming record to be merged has a different apiName.
856
+ * Dynamically generated metric, stored in an {@link RecordApiNameChangeCounters} object.
857
+ *
858
+ * @param context The transaction context.
859
+ *
860
+ * Note: Short-lived metric candidate, remove at the end of 230
861
+ */
862
+ incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
863
+ let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
864
+ if (apiNameChangeCounter === undefined) {
865
+ apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
866
+ this.recordApiNameChangeCounters[existingApiName] = apiNameChangeCounter;
867
+ }
868
+ apiNameChangeCounter.increment(1);
869
+ }
870
+ /**
871
+ * W-8620679
872
+ * Increment the counter for an UnfulfilledSnapshotError coming from luvio
873
+ *
874
+ * @param context The transaction context.
875
+ */
876
+ incrementAdapterRequestErrorCount(context) {
877
+ // We are consolidating all apex adapter instrumentation calls under a single key
878
+ const adapterName = normalizeAdapterName(context.adapterName);
879
+ let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
880
+ if (adapterRequestErrorCounter === undefined) {
881
+ adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
882
+ this.adapterUnfulfilledErrorCounters[adapterName] = adapterRequestErrorCounter;
883
+ }
884
+ adapterRequestErrorCounter.increment(1);
885
+ totalAdapterErrorMetric.increment(1);
524
886
  }
525
887
  }
526
-
527
- class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
528
- constructor() {
529
- super(...arguments);
530
- this.adapterName = 'getRecord';
531
- this.adapterFactory = getRecordAdapterFactory;
888
+ function createMetricsKey(owner, name, unit) {
889
+ let metricName = name;
890
+ if (unit) {
891
+ metricName = metricName + '.' + unit;
532
892
  }
533
- buildConcreteRequest(similarRequest, context) {
534
- return {
535
- ...similarRequest,
536
- config: {
537
- ...similarRequest.config,
538
- recordId: context.recordId,
893
+ return {
894
+ get() {
895
+ return { owner: owner, name: metricName };
896
+ },
897
+ };
898
+ }
899
+ /**
900
+ * Returns whether adapter is an Apex one or not.
901
+ * @param adapterName The name of the adapter.
902
+ */
903
+ function isApexAdapter(adapterName) {
904
+ return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
905
+ }
906
+ /**
907
+ * Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
908
+ * API family, if supplied. Example: `UiApi.getRecord`.
909
+ *
910
+ * Note: If you are adding additional logging that can come from getApex adapter contexts that provide
911
+ * the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
912
+ * ensure to call this method to normalize all logging to 'getApex'. This
913
+ * is because Argus has a 50k key cardinality limit. More context: W-8379680.
914
+ *
915
+ * @param adapterName The name of the adapter.
916
+ * @param apiFamily The API family of the adapter.
917
+ */
918
+ function normalizeAdapterName(adapterName, apiFamily) {
919
+ if (isApexAdapter(adapterName)) {
920
+ return NORMALIZED_APEX_ADAPTER_NAME;
921
+ }
922
+ return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
923
+ }
924
+ const timerMetricTracker = create(null);
925
+ /**
926
+ * Calls instrumentation/service telemetry timer
927
+ * @param name Name of the metric
928
+ * @param duration number to update backing percentile histogram, negative numbers ignored
929
+ */
930
+ function updateTimerMetric(name, duration) {
931
+ let metric = timerMetricTracker[name];
932
+ if (metric === undefined) {
933
+ metric = timer(createMetricsKey(NAMESPACE, name));
934
+ timerMetricTracker[name] = metric;
935
+ }
936
+ timerMetricAddDuration(metric, duration);
937
+ }
938
+ function timerMetricAddDuration(timer, duration) {
939
+ // Guard against negative values since it causes error to be thrown by MetricsService
940
+ if (duration >= 0) {
941
+ timer.addDuration(duration);
942
+ }
943
+ }
944
+ /**
945
+ * W-10315098
946
+ * Increments the counter associated with the request response. Counts are bucketed by status.
947
+ */
948
+ const requestResponseMetricTracker = create(null);
949
+ function incrementRequestResponseCount(cb) {
950
+ const status = cb().status;
951
+ let metric = requestResponseMetricTracker[status];
952
+ if (metric === undefined) {
953
+ metric = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, NETWORK_ADAPTER_RESPONSE_METRIC_NAME, `${status.valueOf()}`));
954
+ requestResponseMetricTracker[status] = metric;
955
+ }
956
+ metric.increment();
957
+ }
958
+ function logObjectInfoChanged() {
959
+ logObjectInfoChanged$1();
960
+ }
961
+ /**
962
+ * Create a new instrumentation cache stats and return it.
963
+ *
964
+ * @param name The cache logger name.
965
+ */
966
+ function registerLdsCacheStats(name) {
967
+ return registerCacheStats(`${NAMESPACE}:${name}`);
968
+ }
969
+ /**
970
+ * Add or overwrite hooks that require aura implementations
971
+ */
972
+ function setAuraInstrumentationHooks() {
973
+ instrument({
974
+ recordConflictsResolved: (serverRequestCount) => {
975
+ // Ignore 0 values which can originate from ADS bridge
976
+ if (serverRequestCount > 0) {
977
+ updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
978
+ }
979
+ },
980
+ nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
981
+ const metricName = `merge-null-dv-count.${fieldType}`;
982
+ if (fieldType === 'scalar') {
983
+ incrementCounterMetric(`${metricName}.${areValuesEqual}`);
984
+ }
985
+ else {
986
+ incrementCounterMetric(metricName);
987
+ }
988
+ },
989
+ getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
990
+ getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
991
+ notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
992
+ notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
993
+ recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
994
+ weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
995
+ getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
996
+ });
997
+ withRegistration$1('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
998
+ instrument$1({
999
+ logCrud: logCRUDLightningInteraction,
1000
+ networkResponse: incrementRequestResponseCount,
1001
+ });
1002
+ instrument$2({
1003
+ refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
1004
+ instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
1005
+ });
1006
+ instrument$3({
1007
+ timerMetricAddDuration: updateTimerMetric,
1008
+ });
1009
+ // Our getRecord through aggregate-ui CRUD logging has moved
1010
+ // to lds-network-adapter. We still need to respect the
1011
+ // orgs environment setting
1012
+ if (forceRecordTransactionsDisabled === false) {
1013
+ ldsNetworkAdapterInstrument({
1014
+ getRecordAggregateResolve: (cb) => {
1015
+ const { recordId, apiName } = cb();
1016
+ logCRUDLightningInteraction('read', {
1017
+ recordId,
1018
+ recordType: apiName,
1019
+ state: 'SUCCESS',
1020
+ });
539
1021
  },
540
- };
1022
+ getRecordAggregateReject: (cb) => {
1023
+ const recordId = cb();
1024
+ logCRUDLightningInteraction('read', {
1025
+ recordId,
1026
+ state: 'ERROR',
1027
+ });
1028
+ },
1029
+ });
1030
+ }
1031
+ withRegistration$1('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
1032
+ }
1033
+ /**
1034
+ * Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
1035
+ *
1036
+ * @param luvio The Luvio instance to instrument.
1037
+ * @param store The InMemoryStore to instrument.
1038
+ */
1039
+ function setupInstrumentation(luvio, store) {
1040
+ setupInstrumentation$1(luvio, store);
1041
+ setAuraInstrumentationHooks();
1042
+ }
1043
+ /**
1044
+ * Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
1045
+ * disable all crud operations if it is on.
1046
+ * @param eventSource - Source of the logging event.
1047
+ * @param attributes - Free form object of attributes to log.
1048
+ */
1049
+ function logCRUDLightningInteraction(eventSource, attributes) {
1050
+ interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
1051
+ }
1052
+ const instrumentation = new Instrumentation();
1053
+
1054
+ class ApplicationPredictivePrefetcher {
1055
+ constructor(context, repository, requestRunner) {
1056
+ this.repository = repository;
1057
+ this.requestRunner = requestRunner;
1058
+ this.isRecording = false;
1059
+ this.queuedPredictionRequests = [];
1060
+ this._context = context;
1061
+ this.page = this.getPage();
1062
+ }
1063
+ set context(value) {
1064
+ this._context = value;
1065
+ this.page = this.getPage();
1066
+ }
1067
+ get context() {
1068
+ return this._context;
1069
+ }
1070
+ async stopRecording() {
1071
+ this.isRecording = false;
1072
+ await this.repository.flushRequestsToStorage();
1073
+ }
1074
+ startRecording() {
1075
+ this.isRecording = true;
1076
+ this.repository.clearRequestBuffer();
1077
+ }
1078
+ saveRequest(request) {
1079
+ if (!this.isRecording) {
1080
+ return Promise.resolve();
1081
+ }
1082
+ return executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_SAVE_REQUEST, (_act) => {
1083
+ const { request: requestToSave, context } = this.page.buildSaveRequestData(request);
1084
+ // No need to differentiate from predictions requests because these
1085
+ // are made from the adapters factory, which are not prediction aware.
1086
+ return this.repository.saveRequest(context, requestToSave);
1087
+ });
1088
+ }
1089
+ async predict() {
1090
+ const exactPageRequests = (await this.repository.getPageRequests(this.context)) || [];
1091
+ const similarPageRequests = await this.getSimilarPageRequests();
1092
+ const alwaysRequests = this.page.getAlwaysRunRequests();
1093
+ const predictedRequests = [
1094
+ ...alwaysRequests,
1095
+ ...this.requestRunner.reduceRequests([
1096
+ ...exactPageRequests,
1097
+ ...similarPageRequests,
1098
+ ...this.page.getAlwaysRunRequests(),
1099
+ ]),
1100
+ ];
1101
+ this.queuedPredictionRequests.push(...predictedRequests);
1102
+ return Promise.all(predictedRequests.map((request) => this.requestRunner.runRequest(request))).then();
1103
+ }
1104
+ getPredictionSummary() {
1105
+ const exactPageRequests = this.repository.getPageRequests(this.context) || [];
1106
+ const similarPageRequests = this.page.similarContext !== undefined
1107
+ ? this.repository.getPageRequests(this.page.similarContext)
1108
+ : [];
1109
+ return { exact: exactPageRequests.length, similar: similarPageRequests.length };
1110
+ }
1111
+ hasPredictions() {
1112
+ const summary = this.getPredictionSummary();
1113
+ return summary.exact > 0 || summary.similar > 0;
1114
+ }
1115
+ getSimilarPageRequests() {
1116
+ let resolvedSimilarPageRequests = [];
1117
+ if (this.page.similarContext !== undefined) {
1118
+ const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
1119
+ if (similarPageRequests !== undefined) {
1120
+ resolvedSimilarPageRequests = similarPageRequests.map((request) => this.page.resolveSimilarRequest(request));
1121
+ }
1122
+ }
1123
+ return resolvedSimilarPageRequests;
1124
+ }
1125
+ }
1126
+
1127
+ class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
1128
+ constructor(context, repository, requestRunner,
1129
+ // These strategies need to be in sync with the "predictiveDataLoadCapable" list
1130
+ // from scripts/lds-uiapi-plugin.js
1131
+ requestStrategies) {
1132
+ super(context, repository, requestRunner);
1133
+ this.requestStrategies = requestStrategies;
1134
+ this.page = this.getPage();
541
1135
  }
542
- transformForSave(request) {
543
- if (request.config.fields === undefined && request.config.optionalFields === undefined) {
544
- return request;
1136
+ getPage() {
1137
+ if (RecordHomePage.handlesContext(this.context)) {
1138
+ return new RecordHomePage(this.context, this.requestStrategies);
545
1139
  }
546
- let fields = coerceFieldIdArray(request.config.fields) || [];
547
- let optionalFields = coerceFieldIdArray(request.config.optionalFields) || [];
548
- return {
549
- ...request,
550
- config: {
551
- ...request.config,
552
- fields: undefined,
553
- optionalFields: [...fields, ...optionalFields],
554
- },
555
- };
1140
+ return new LexDefaultPage(this.context);
556
1141
  }
557
- canCombine(reqA, reqB) {
558
- // must be same record and
559
- return (reqA.recordId === reqB.recordId &&
560
- // both requests are fields requests
561
- (reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
562
- (reqB.fields !== undefined || reqB.optionalFields !== undefined));
1142
+ }
1143
+
1144
+ // Copy-pasted from adapter-utils. This util should be extracted from generated code and imported in prefetch repository.
1145
+ const { keys: ObjectKeys$2 } = Object;
1146
+ const { stringify: JSONStringify } = JSON;
1147
+ const { isArray: ArrayIsArray$1 } = Array;
1148
+ /**
1149
+ * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
1150
+ * This is needed because insertion order for JSON.stringify(object) affects output:
1151
+ * JSON.stringify({a: 1, b: 2})
1152
+ * "{"a":1,"b":2}"
1153
+ * JSON.stringify({b: 2, a: 1})
1154
+ * "{"b":2,"a":1}"
1155
+ * @param data Data to be JSON-stringified.
1156
+ * @returns JSON.stringified value with consistent ordering of keys.
1157
+ */
1158
+ function stableJSONStringify(node) {
1159
+ // This is for Date values.
1160
+ if (node && node.toJSON && typeof node.toJSON === 'function') {
1161
+ // eslint-disable-next-line no-param-reassign
1162
+ node = node.toJSON();
563
1163
  }
564
- combineRequests(reqA, reqB) {
565
- const fields = new Set();
566
- const optionalFields = new Set();
567
- if (reqA.fields !== undefined) {
568
- reqA.fields.forEach((field) => fields.add(field));
1164
+ if (node === undefined) {
1165
+ return;
1166
+ }
1167
+ if (typeof node === 'number') {
1168
+ return isFinite(node) ? '' + node : 'null';
1169
+ }
1170
+ if (typeof node !== 'object') {
1171
+ return JSONStringify(node);
1172
+ }
1173
+ let i;
1174
+ let out;
1175
+ if (ArrayIsArray$1(node)) {
1176
+ out = '[';
1177
+ for (i = 0; i < node.length; i++) {
1178
+ if (i) {
1179
+ out += ',';
1180
+ }
1181
+ out += stableJSONStringify(node[i]) || 'null';
569
1182
  }
570
- if (reqB.fields !== undefined) {
571
- reqB.fields.forEach((field) => fields.add(field));
1183
+ return out + ']';
1184
+ }
1185
+ if (node === null) {
1186
+ return 'null';
1187
+ }
1188
+ const keys = ObjectKeys$2(node).sort();
1189
+ out = '';
1190
+ for (i = 0; i < keys.length; i++) {
1191
+ const key = keys[i];
1192
+ const value = stableJSONStringify(node[key]);
1193
+ if (!value) {
1194
+ continue;
572
1195
  }
573
- if (reqA.optionalFields !== undefined) {
574
- reqA.optionalFields.forEach((field) => optionalFields.add(field));
1196
+ if (out) {
1197
+ out += ',';
575
1198
  }
576
- if (reqB.optionalFields !== undefined) {
577
- reqB.optionalFields.forEach((field) => optionalFields.add(field));
1199
+ out += JSONStringify(key) + ':' + value;
1200
+ }
1201
+ return '{' + out + '}';
1202
+ }
1203
+ function isObject(obj) {
1204
+ return obj !== null && typeof obj === 'object';
1205
+ }
1206
+ function deepEquals(objA, objB) {
1207
+ if (objA === objB)
1208
+ return true;
1209
+ if (objA instanceof Date && objB instanceof Date)
1210
+ return objA.getTime() === objB.getTime();
1211
+ // If one of them is not an object, they are not deeply equal
1212
+ if (!isObject(objA) || !isObject(objB))
1213
+ return false;
1214
+ // Filter out keys set as undefined, we can compare undefined as equals.
1215
+ const keysA = ObjectKeys$2(objA).filter((key) => objA[key] !== undefined);
1216
+ const keysB = ObjectKeys$2(objB).filter((key) => objB[key] !== undefined);
1217
+ // If the objects do not have the same set of keys, they are not deeply equal
1218
+ if (keysA.length !== keysB.length)
1219
+ return false;
1220
+ for (const key of keysA) {
1221
+ const valA = objA[key];
1222
+ const valB = objB[key];
1223
+ const areObjects = isObject(valA) && isObject(valB);
1224
+ // If both values are objects, recursively compare them
1225
+ if (areObjects && !deepEquals(valA, valB))
1226
+ return false;
1227
+ // If only one value is an object or if the values are not strictly equal, they are not deeply equal
1228
+ if (!areObjects && valA !== valB)
1229
+ return false;
1230
+ }
1231
+ return true;
1232
+ }
1233
+
1234
+ class PrefetchRepository {
1235
+ constructor(storage) {
1236
+ this.storage = storage;
1237
+ this.requestBuffer = new Map();
1238
+ }
1239
+ clearRequestBuffer() {
1240
+ this.requestBuffer.clear();
1241
+ }
1242
+ async flushRequestsToStorage() {
1243
+ const setPromises = [];
1244
+ for (const [id, batch] of this.requestBuffer) {
1245
+ const page = { id, requests: [] };
1246
+ batch.forEach(({ request, requestTime }) => {
1247
+ const existingRequestEntry = page.requests.find(({ request: storedRequest }) => deepEquals(storedRequest, request));
1248
+ if (existingRequestEntry === undefined) {
1249
+ page.requests.push({
1250
+ request,
1251
+ requestMetadata: {
1252
+ requestTime,
1253
+ },
1254
+ });
1255
+ }
1256
+ else if (requestTime < existingRequestEntry.requestMetadata.requestTime) {
1257
+ existingRequestEntry.requestMetadata.requestTime = requestTime;
1258
+ }
1259
+ });
1260
+ setPromises.push(this.storage.set(id, page));
578
1261
  }
579
- return {
580
- recordId: reqA.recordId,
581
- fields: Array.from(fields),
582
- optionalFields: Array.from(optionalFields),
583
- };
1262
+ this.clearRequestBuffer();
1263
+ await Promise.all(setPromises);
584
1264
  }
585
- isContextDependent(context, request) {
586
- return request.config.recordId === context.recordId;
1265
+ getKeyId(key) {
1266
+ return stableJSONStringify(key);
587
1267
  }
588
- buildSaveRequestData(similarContext, context, request) {
589
- if (this.isContextDependent(context, request)) {
590
- return {
591
- request: this.transformForSave({
592
- ...request,
593
- config: {
594
- ...request.config,
595
- recordId: '*',
596
- },
597
- }),
598
- context: similarContext,
599
- };
1268
+ async saveRequest(key, request) {
1269
+ const identifier = this.getKeyId(key);
1270
+ const batchForKey = this.requestBuffer.get(identifier) || [];
1271
+ batchForKey.push({
1272
+ request,
1273
+ requestTime: Date.now(),
1274
+ });
1275
+ this.requestBuffer.set(identifier, batchForKey);
1276
+ }
1277
+ getPage(key) {
1278
+ const identifier = stableJSONStringify(key);
1279
+ return this.storage.get(identifier);
1280
+ }
1281
+ getPageRequests(key) {
1282
+ const page = this.getPage(key);
1283
+ if (page === undefined) {
1284
+ return [];
600
1285
  }
601
- return {
602
- request: this.transformForSave(request),
603
- context,
604
- };
1286
+ return page.requests.map((requestEntry) => requestEntry.request);
605
1287
  }
606
1288
  }
607
1289
 
608
- class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
609
- constructor() {
610
- super(...arguments);
611
- this.adapterName = 'getRecords';
612
- this.adapterFactory = getRecordsAdapterFactory;
1290
+ class RequestStrategy {
1291
+ transformForSave(request) {
1292
+ return request;
613
1293
  }
614
- buildConcreteRequest(similarRequest, context) {
615
- return {
616
- ...similarRequest,
617
- config: {
618
- ...similarRequest.config,
619
- records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
620
- },
621
- };
1294
+ reduce(requests) {
1295
+ return requests;
622
1296
  }
623
- isContextDependent(context, request) {
624
- const isSingleRecordRequest = request.config.records.length === 1 && request.config.records[0].recordIds.length === 1;
625
- return isSingleRecordRequest && request.config.records[0].recordIds[0] === context.recordId;
1297
+ }
1298
+
1299
+ class LuvioAdapterRequestStrategy extends RequestStrategy {
1300
+ /**
1301
+ * Perform any transformations required to prepare the request for saving.
1302
+ *
1303
+ * e.g. If the request is for a record, we move all fields in the fields array
1304
+ * into the optionalFields array
1305
+ *
1306
+ * @param request - The request to transform
1307
+ * @returns
1308
+ */
1309
+ transformForSave(request) {
1310
+ return request;
1311
+ }
1312
+ /**
1313
+ * Filter requests to only those that are for this strategy.
1314
+ *
1315
+ * @param unfilteredRequests array of requests to filter
1316
+ * @returns
1317
+ */
1318
+ filterRequests(unfilteredRequests) {
1319
+ return unfilteredRequests.filter((request) => request.adapterName === this.adapterName);
1320
+ }
1321
+ /**
1322
+ * Reduce requests by combining those based on a strategies implementations
1323
+ * of canCombine and combineRequests.
1324
+ * @param unfilteredRequests array of requests to filter
1325
+ * @returns
1326
+ */
1327
+ reduce(unfilteredRequests) {
1328
+ const requests = this.filterRequests(unfilteredRequests);
1329
+ const visitedRequests = new Set();
1330
+ const reducedRequests = [];
1331
+ for (let i = 0, n = requests.length; i < n; i++) {
1332
+ const currentRequest = requests[i];
1333
+ if (!visitedRequests.has(currentRequest)) {
1334
+ const combinedRequest = { ...currentRequest };
1335
+ for (let j = i + 1; j < n; j++) {
1336
+ const hasNotBeenVisited = !visitedRequests.has(requests[j]);
1337
+ const canCombineConfigs = this.canCombine(combinedRequest.config, requests[j].config);
1338
+ if (hasNotBeenVisited && canCombineConfigs) {
1339
+ combinedRequest.config = this.combineRequests(combinedRequest.config, requests[j].config);
1340
+ visitedRequests.add(requests[j]);
1341
+ }
1342
+ }
1343
+ reducedRequests.push(combinedRequest);
1344
+ visitedRequests.add(currentRequest);
1345
+ }
1346
+ }
1347
+ return reducedRequests;
1348
+ }
1349
+ /**
1350
+ * Check if two requests can be combined.
1351
+ *
1352
+ * By default, requests are not combinable.
1353
+ * @param reqA config of request A
1354
+ * @param reqB config of request B
1355
+ * @returns
1356
+ */
1357
+ canCombine(_reqA, _reqB) {
1358
+ return false;
626
1359
  }
627
- buildSaveRequestData(similarContext, context, request) {
628
- if (this.isContextDependent(context, request)) {
629
- return {
630
- request: this.transformForSave({
631
- ...request,
632
- config: {
633
- ...request.config,
634
- records: [
635
- {
636
- ...request.config.records[0],
637
- recordIds: ['*'],
638
- },
639
- ],
640
- },
641
- }),
642
- context: similarContext,
643
- };
1360
+ /**
1361
+ * Takes two request configs and combines them into a single request config.
1362
+ *
1363
+ * @param reqA config of request A
1364
+ * @param reqB config of request B
1365
+ * @returns
1366
+ */
1367
+ combineRequests(reqA, _reqB) {
1368
+ // By default, this should never be called since requests aren't combinable
1369
+ if (process.env.NODE_ENV !== 'production') {
1370
+ throw new Error('Not implemented');
644
1371
  }
1372
+ return reqA;
1373
+ }
1374
+ /**
1375
+ * Checks adapter config against request context to determine if the request is context dependent.
1376
+ *
1377
+ * By default, requests are not context dependent.
1378
+ * @param request
1379
+ * @returns
1380
+ */
1381
+ isContextDependent(_context, _request) {
1382
+ return false;
1383
+ }
1384
+ /**
1385
+ * Builds request for saving,
1386
+ * - transforming the request
1387
+ * - handling the cases where the request is context dependent (this is homework for the subclass)
1388
+ * @param _similarContext Context with at least one parameter as a wildcard '*'
1389
+ * @param context Exact context for a given page
1390
+ * @param request
1391
+ * @returns
1392
+ */
1393
+ buildSaveRequestData(_similarContext, context, request) {
645
1394
  return {
646
1395
  request: this.transformForSave(request),
647
- context: context,
1396
+ context,
648
1397
  };
649
1398
  }
650
1399
  }
651
1400
 
652
- function normalizeRecordIds(recordIds) {
653
- if (!ArrayIsArray$1(recordIds)) {
1401
+ function normalizeRecordIds$1(recordIds) {
1402
+ if (!Array.isArray(recordIds)) {
654
1403
  return [recordIds];
655
1404
  }
656
1405
  return recordIds;
657
1406
  }
658
- function normalizeApiNames(apiNames) {
659
- if (apiNames === undefined || apiNames === null) {
660
- return [];
661
- }
662
- return ArrayIsArray$1(apiNames) ? apiNames : [apiNames];
663
- }
664
- class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
1407
+ class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
665
1408
  constructor() {
666
1409
  super(...arguments);
667
- this.adapterName = 'getRecordActions';
668
- this.adapterFactory = getRecordActionsAdapterFactory;
1410
+ this.adapterName = 'getRecordAvatars';
1411
+ this.adapterFactory = getRecordAvatarsAdapterFactory;
669
1412
  }
670
1413
  buildConcreteRequest(similarRequest, context) {
671
1414
  return {
@@ -694,99 +1437,83 @@ class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
694
1437
  context,
695
1438
  };
696
1439
  }
697
- canCombine(reqA, reqB) {
698
- return (reqA.retrievalMode === reqB.retrievalMode &&
699
- reqA.formFactor === reqB.formFactor &&
700
- (reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
701
- (reqA.sections || []).toString() === (reqB.sections || []).toString());
702
- }
703
- combineRequests(reqA, reqB) {
704
- const combined = { ...reqA };
705
- // let's merge the recordIds
706
- combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
707
- if (combined.retrievalMode === 'ALL') {
708
- const combinedSet = new Set([
709
- ...normalizeApiNames(combined.apiNames),
710
- ...normalizeApiNames(reqB.apiNames),
711
- ]);
712
- combined.apiNames = Array.from(combinedSet);
713
- }
714
- return combined;
715
- }
716
1440
  isContextDependent(context, request) {
717
1441
  return (request.config.recordIds &&
718
1442
  (context.recordId === request.config.recordIds || // some may set this as string instead of array
719
1443
  (request.config.recordIds.length === 1 &&
720
1444
  request.config.recordIds[0] === context.recordId)));
721
1445
  }
1446
+ canCombine(reqA, reqB) {
1447
+ return reqA.formFactor === reqB.formFactor;
1448
+ }
1449
+ combineRequests(reqA, reqB) {
1450
+ const combined = { ...reqA };
1451
+ combined.recordIds = Array.from(new Set([...normalizeRecordIds$1(reqA.recordIds), ...normalizeRecordIds$1(reqB.recordIds)]));
1452
+ return combined;
1453
+ }
722
1454
  }
723
1455
 
724
- class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
1456
+ class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
725
1457
  constructor() {
726
1458
  super(...arguments);
727
- this.adapterName = 'getObjectInfo';
728
- this.adapterFactory = getObjectInfoAdapterFactory;
1459
+ this.adapterName = 'getRecord';
1460
+ this.adapterFactory = getRecordAdapterFactory;
729
1461
  }
730
1462
  buildConcreteRequest(similarRequest, context) {
731
1463
  return {
732
1464
  ...similarRequest,
733
1465
  config: {
734
1466
  ...similarRequest.config,
735
- objectApiName: context.objectApiName,
1467
+ recordId: context.recordId,
736
1468
  },
737
1469
  };
738
1470
  }
739
- buildSaveRequestData(similarContext, context, request) {
740
- if (this.isContextDependent(context, request)) {
741
- return {
742
- request: this.transformForSave(request),
743
- context: similarContext,
744
- };
1471
+ transformForSave(request) {
1472
+ if (request.config.fields === undefined && request.config.optionalFields === undefined) {
1473
+ return request;
745
1474
  }
1475
+ let fields = coerceFieldIdArray(request.config.fields) || [];
1476
+ let optionalFields = coerceFieldIdArray(request.config.optionalFields) || [];
746
1477
  return {
747
- request: this.transformForSave(request),
748
- context,
1478
+ ...request,
1479
+ config: {
1480
+ ...request.config,
1481
+ fields: undefined,
1482
+ optionalFields: [...fields, ...optionalFields],
1483
+ },
749
1484
  };
750
1485
  }
751
- isContextDependent(context, request) {
752
- return (request.config.objectApiName !== undefined &&
753
- context.objectApiName === request.config.objectApiName);
754
- }
755
- }
756
-
757
- class GetObjectInfosRequestStrategy extends LuvioAdapterRequestStrategy {
758
- constructor() {
759
- super(...arguments);
760
- this.adapterName = 'getObjectInfos';
761
- this.adapterFactory = getObjectInfosAdapterFactory;
762
- }
763
- buildConcreteRequest(similarRequest) {
764
- return similarRequest;
765
- }
766
- }
767
-
768
- const { keys: ObjectKeys$1 } = Object;
769
- const { isArray: ArrayIsArray, from: ArrayFrom } = Array;
770
- function isReduceAbleRelatedListConfig(config) {
771
- return config.relatedListsActionParameters.every((rlReq) => {
772
- return rlReq.relatedListId !== undefined && ObjectKeys$1(rlReq).length === 1;
773
- });
774
- }
775
- class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy {
776
- constructor() {
777
- super(...arguments);
778
- this.adapterName = 'getRelatedListsActions';
779
- this.adapterFactory = getRelatedListsActionsAdapterFactory;
1486
+ canCombine(reqA, reqB) {
1487
+ // must be same record and
1488
+ return (reqA.recordId === reqB.recordId &&
1489
+ // both requests are fields requests
1490
+ (reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
1491
+ (reqB.fields !== undefined || reqB.optionalFields !== undefined));
780
1492
  }
781
- buildConcreteRequest(similarRequest, context) {
1493
+ combineRequests(reqA, reqB) {
1494
+ const fields = new Set();
1495
+ const optionalFields = new Set();
1496
+ if (reqA.fields !== undefined) {
1497
+ reqA.fields.forEach((field) => fields.add(field));
1498
+ }
1499
+ if (reqB.fields !== undefined) {
1500
+ reqB.fields.forEach((field) => fields.add(field));
1501
+ }
1502
+ if (reqA.optionalFields !== undefined) {
1503
+ reqA.optionalFields.forEach((field) => optionalFields.add(field));
1504
+ }
1505
+ if (reqB.optionalFields !== undefined) {
1506
+ reqB.optionalFields.forEach((field) => optionalFields.add(field));
1507
+ }
782
1508
  return {
783
- ...similarRequest,
784
- config: {
785
- ...similarRequest.config,
786
- recordIds: [context.recordId],
787
- },
1509
+ recordId: reqA.recordId,
1510
+ fields: Array.from(fields),
1511
+ optionalFields: Array.from(optionalFields),
788
1512
  };
789
1513
  }
1514
+ isContextDependent(context, request) {
1515
+ return request.config.recordId === context.recordId;
1516
+ }
790
1517
  buildSaveRequestData(similarContext, context, request) {
791
1518
  if (this.isContextDependent(context, request)) {
792
1519
  return {
@@ -794,7 +1521,7 @@ class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy
794
1521
  ...request,
795
1522
  config: {
796
1523
  ...request.config,
797
- recordIds: ['*'],
1524
+ recordId: '*',
798
1525
  },
799
1526
  }),
800
1527
  context: similarContext,
@@ -803,84 +1530,29 @@ class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy
803
1530
  return {
804
1531
  request: this.transformForSave(request),
805
1532
  context,
806
- };
807
- }
808
- isContextDependent(context, request) {
809
- const isForContext = request.config.recordIds &&
810
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
811
- (request.config.recordIds.length === 1 &&
812
- request.config.recordIds[0] === context.recordId));
813
- return isForContext && isReduceAbleRelatedListConfig(request.config);
814
- }
815
- /**
816
- * Can only reduce two requests when they have the same recordId, and
817
- * the individual relatedListAction config only have relatedListId.
818
- *
819
- * @param reqA
820
- * @param reqB
821
- * @returns boolean
822
- */
823
- canCombine(reqA, reqB) {
824
- const [recordIdA, recordIdB] = [reqA.recordIds, reqB.recordIds].map((recordIds) => {
825
- return ArrayIsArray(recordIds)
826
- ? recordIds.length === 1
827
- ? recordIds[0]
828
- : null
829
- : recordIds;
830
- });
831
- return (recordIdA === recordIdB &&
832
- recordIdA !== null &&
833
- isReduceAbleRelatedListConfig(reqA) &&
834
- isReduceAbleRelatedListConfig(reqB));
835
- }
836
- combineRequests(reqA, reqB) {
837
- const relatedListsIncluded = new Set();
838
- [reqA, reqB].forEach(({ relatedListsActionParameters }) => {
839
- relatedListsActionParameters.forEach(({ relatedListId }) => relatedListsIncluded.add(relatedListId));
840
- });
841
- return {
842
- recordIds: reqA.recordIds,
843
- relatedListsActionParameters: ArrayFrom(relatedListsIncluded).map((relatedListId) => ({
844
- relatedListId,
845
- })),
846
- };
847
- }
848
- }
849
-
850
- class GetRelatedListInfoBatchRequestStrategy extends LuvioAdapterRequestStrategy {
851
- constructor() {
852
- super(...arguments);
853
- this.adapterName = 'getRelatedListInfoBatch';
854
- this.adapterFactory = getRelatedListInfoBatchAdapterFactory;
855
- }
856
- buildConcreteRequest(similarRequest, _context) {
857
- return similarRequest;
858
- }
859
- canCombine(reqA, reqB) {
860
- return reqA.parentObjectApiName === reqB.parentObjectApiName;
861
- }
862
- combineRequests(reqA, reqB) {
863
- const combined = { ...reqA };
864
- combined.relatedListNames = Array.from(new Set([...reqA.relatedListNames, ...reqB.relatedListNames]));
865
- return combined;
1533
+ };
866
1534
  }
867
1535
  }
868
1536
 
869
- class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
1537
+ class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
870
1538
  constructor() {
871
1539
  super(...arguments);
872
- this.adapterName = 'getRelatedListRecords';
873
- this.adapterFactory = getRelatedListRecordsAdapterFactory;
1540
+ this.adapterName = 'getRecords';
1541
+ this.adapterFactory = getRecordsAdapterFactory;
874
1542
  }
875
1543
  buildConcreteRequest(similarRequest, context) {
876
1544
  return {
877
1545
  ...similarRequest,
878
1546
  config: {
879
1547
  ...similarRequest.config,
880
- parentRecordId: context.recordId,
1548
+ records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
881
1549
  },
882
1550
  };
883
1551
  }
1552
+ isContextDependent(context, request) {
1553
+ const isSingleRecordRequest = request.config.records.length === 1 && request.config.records[0].recordIds.length === 1;
1554
+ return isSingleRecordRequest && request.config.records[0].recordIds[0] === context.recordId;
1555
+ }
884
1556
  buildSaveRequestData(similarContext, context, request) {
885
1557
  if (this.isContextDependent(context, request)) {
886
1558
  return {
@@ -888,7 +1560,12 @@ class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
888
1560
  ...request,
889
1561
  config: {
890
1562
  ...request.config,
891
- parentRecordId: '*',
1563
+ records: [
1564
+ {
1565
+ ...request.config.records[0],
1566
+ recordIds: ['*'],
1567
+ },
1568
+ ],
892
1569
  },
893
1570
  }),
894
1571
  context: similarContext,
@@ -896,29 +1573,35 @@ class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
896
1573
  }
897
1574
  return {
898
1575
  request: this.transformForSave(request),
899
- context,
1576
+ context: context,
900
1577
  };
901
1578
  }
902
- // ADG currently handles the batching of GetRelatedListRecords -> GetRelatedListRecordsBatch
903
- // https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListRecordsBatchReducer.js
904
- // Adding reducing is outside of the scope of this PR, but this could potentially be added in the future
905
- isContextDependent(context, request) {
906
- return context.recordId === request.config.parentRecordId;
907
- }
908
1579
  }
909
1580
 
910
- class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrategy {
1581
+ function normalizeRecordIds(recordIds) {
1582
+ if (!ArrayIsArray$1(recordIds)) {
1583
+ return [recordIds];
1584
+ }
1585
+ return recordIds;
1586
+ }
1587
+ function normalizeApiNames(apiNames) {
1588
+ if (apiNames === undefined || apiNames === null) {
1589
+ return [];
1590
+ }
1591
+ return ArrayIsArray$1(apiNames) ? apiNames : [apiNames];
1592
+ }
1593
+ class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
911
1594
  constructor() {
912
1595
  super(...arguments);
913
- this.adapterName = 'getRelatedListRecordsBatch';
914
- this.adapterFactory = getRelatedListRecordsBatchAdapterFactory;
1596
+ this.adapterName = 'getRecordActions';
1597
+ this.adapterFactory = getRecordActionsAdapterFactory;
915
1598
  }
916
1599
  buildConcreteRequest(similarRequest, context) {
917
1600
  return {
918
1601
  ...similarRequest,
919
1602
  config: {
920
1603
  ...similarRequest.config,
921
- parentRecordId: context.recordId,
1604
+ recordIds: [context.recordId],
922
1605
  },
923
1606
  };
924
1607
  }
@@ -929,7 +1612,7 @@ class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrat
929
1612
  ...request,
930
1613
  config: {
931
1614
  ...request.config,
932
- parentRecordId: '*',
1615
+ recordIds: ['*'],
933
1616
  },
934
1617
  }),
935
1618
  context: similarContext,
@@ -940,710 +1623,412 @@ class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrat
940
1623
  context,
941
1624
  };
942
1625
  }
943
- isContextDependent(context, request) {
944
- return context.recordId === request.config.parentRecordId;
945
- }
946
- /**
947
- * Can combine two seperate batch requests if the parentRecordId is the same.
948
- * @param reqA The first GetRelatedListRecordsBatchConfig.
949
- * @param reqB The first GetRelatedListRecordsBatchConfig.
950
- * @returns true if the requests can be combined, otherwise false.
951
- */
952
1626
  canCombine(reqA, reqB) {
953
- return reqA.parentRecordId === reqB.parentRecordId;
1627
+ return (reqA.retrievalMode === reqB.retrievalMode &&
1628
+ reqA.formFactor === reqB.formFactor &&
1629
+ (reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
1630
+ (reqA.sections || []).toString() === (reqB.sections || []).toString());
954
1631
  }
955
- /**
956
- * Merge the relatedListParameters together between two combinable batch requests.
957
- * @param reqA The first GetRelatedListRecordsBatchConfig.
958
- * @param reqB The first GetRelatedListRecordsBatchConfig.
959
- * @returns The combined request.
960
- */
961
1632
  combineRequests(reqA, reqB) {
962
- const relatedListParametersMap = new Set(reqA.relatedListParameters.map((relatedListParameter) => {
963
- return stableJSONStringify$1(relatedListParameter);
964
- }));
965
- const reqBRelatedListParametersToAdd = reqB.relatedListParameters.filter((relatedListParameter) => {
966
- return !relatedListParametersMap.has(stableJSONStringify$1(relatedListParameter));
967
- });
968
- reqA.relatedListParameters = reqA.relatedListParameters.concat(reqBRelatedListParametersToAdd);
969
- return reqA;
970
- }
971
- }
972
-
973
- class LexRequestRunner {
974
- constructor(luvio) {
975
- this.luvio = luvio;
976
- this.requestStrategies = {
977
- getRecord: new GetRecordRequestStrategy(),
978
- getRecords: new GetRecordsRequestStrategy(),
979
- getRecordActions: new GetRecordActionsRequestStrategy(),
980
- getRecordAvatars: new GetRecordAvatarsRequestStrategy(),
981
- getObjectInfo: new GetObjectInfoRequestStrategy(),
982
- getObjectInfos: new GetObjectInfosRequestStrategy(),
983
- getRelatedListsActions: new GetRelatedListsActionsRequestStrategy(),
984
- getRelatedListInfoBatch: new GetRelatedListInfoBatchRequestStrategy(),
985
- getRelatedListRecords: new GetRelatedListRecordsRequestStrategy(),
986
- getRelatedListRecordsBatch: new GetRelatedListRecordsBatchRequestStrategy(),
987
- };
988
- }
989
- reduceRequests(requests) {
990
- return Object.values(this.requestStrategies)
991
- .map((strategy) => strategy.reduce(requests))
992
- .flat();
993
- }
994
- runRequest(request) {
995
- if (request.adapterName in this.requestStrategies) {
996
- const adapterFactory = this.requestStrategies[request.adapterName].adapterFactory;
997
- return Promise.resolve(adapterFactory(this.luvio)(request.config)).then();
1633
+ const combined = { ...reqA };
1634
+ // let's merge the recordIds
1635
+ combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
1636
+ if (combined.retrievalMode === 'ALL') {
1637
+ const combinedSet = new Set([
1638
+ ...normalizeApiNames(combined.apiNames),
1639
+ ...normalizeApiNames(reqB.apiNames),
1640
+ ]);
1641
+ combined.apiNames = Array.from(combinedSet);
998
1642
  }
999
- return Promise.resolve(undefined);
1000
- }
1001
- }
1002
-
1003
- class InMemoryPrefetchStorage {
1004
- constructor() {
1005
- this.data = {};
1006
- }
1007
- set(key, value) {
1008
- this.data[key] = value;
1009
- return Promise.resolve();
1010
- }
1011
- get(key) {
1012
- return this.data[key];
1013
- }
1014
- }
1015
-
1016
- const { keys: ObjectKeys } = Object;
1017
- const DEFAULT_STORAGE_OPTIONS = {
1018
- name: 'ldsPredictiveLoading',
1019
- persistent: true,
1020
- secure: true,
1021
- maxSize: 7 * 1024 * 1024,
1022
- expiration: 12 * 60 * 60,
1023
- clearOnInit: false,
1024
- debugLogging: false,
1025
- version: 2,
1026
- };
1027
- function buildAuraPrefetchStorage(options = {}) {
1028
- const auraStorage = createStorage({
1029
- ...DEFAULT_STORAGE_OPTIONS,
1030
- ...options,
1031
- });
1032
- const inMemoryStorage = new InMemoryPrefetchStorage();
1033
- if (auraStorage === null) {
1034
- return inMemoryStorage;
1035
- }
1036
- return new AuraPrefetchStorage(auraStorage, inMemoryStorage);
1037
- }
1038
- class AuraPrefetchStorage {
1039
- constructor(auraStorage, inMemoryStorage) {
1040
- this.auraStorage = auraStorage;
1041
- this.inMemoryStorage = inMemoryStorage;
1042
- /**
1043
- * Because of aura is an event loop hog and we therefore need to minimize asynchronicity in LEX,
1044
- * we need need to preload predictions and treat read operations sync. Not making it sync, will cause
1045
- * some request to be sent the network when they could be dedupe against those from the predictions.
1046
- *
1047
- * Drawbacks of this approach:
1048
- * 1. Loading all of aura storage into memory and then updating it based on changes to that in memory
1049
- * representation means that updates to predictions in aura storage across different tabs will result
1050
- * in overwrites, not graceful merges of predictions.
1051
- * 2. If whoever is consuming this tries to get and run predictions before this is done loading,
1052
- * then they will (potentially incorrectly) think that we don't have any predictions.
1053
- */
1054
- auraStorage.getAll().then((results) => {
1055
- ObjectKeys(results).forEach((key) => this.inMemoryStorage.set(key, results[key]));
1056
- });
1057
- }
1058
- set(key, value) {
1059
- const inMemoryResult = this.inMemoryStorage.set(key, value);
1060
- this.auraStorage.set(key, value).catch((error) => {
1061
- if (process.env.NODE_ENV !== 'production') {
1062
- // eslint-disable-next-line no-console
1063
- console.error('Error save LDS prediction: ', error);
1064
- }
1065
- });
1066
- return inMemoryResult;
1067
- }
1068
- get(key) {
1069
- // we never read from the AuraStorage, except in construction.
1070
- return this.inMemoryStorage.get(key);
1071
- }
1072
- }
1073
-
1074
- /**
1075
- * Observability / Critical Availability Program (230+)
1076
- *
1077
- * This file is intended to be used as a consolidated place for all definitions, functions,
1078
- * and helpers related to "M1"[1].
1079
- *
1080
- * Below are the R.E.A.D.S. metrics for the Lightning Data Service, defined here[2].
1081
- *
1082
- * [1] Search "[M1] Lightning Data Service Design Spike" in Quip
1083
- * [2] Search "Lightning Data Service R.E.A.D.S. Metrics" in Quip
1084
- */
1085
- const OBSERVABILITY_NAMESPACE = 'LIGHTNING.lds.service';
1086
- const ADAPTER_INVOCATION_COUNT_METRIC_NAME = 'request';
1087
- const ADAPTER_ERROR_COUNT_METRIC_NAME = 'error';
1088
- const NETWORK_ADAPTER_RESPONSE_METRIC_NAME = 'network-response';
1089
- /**
1090
- * W-8379680
1091
- * Counter for number of getApex requests.
1092
- */
1093
- const GET_APEX_REQUEST_COUNT = {
1094
- get() {
1095
- return {
1096
- owner: OBSERVABILITY_NAMESPACE,
1097
- name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
1098
- };
1099
- },
1100
- };
1101
- /**
1102
- * W-8828410
1103
- * Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
1104
- */
1105
- const TOTAL_ADAPTER_ERROR_COUNT = {
1106
- get() {
1107
- return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
1108
- },
1109
- };
1110
- /**
1111
- * W-8828410
1112
- * Counter for the number of invocations made into LDS by a wire adapter.
1113
- */
1114
- const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
1115
- get() {
1116
- return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
1117
- },
1118
- };
1119
-
1120
- const { create, keys } = Object;
1121
- const { isArray } = Array;
1122
- const { stringify } = JSON;
1123
-
1124
- /**
1125
- * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
1126
- * This is needed because insertion order for JSON.stringify(object) affects output:
1127
- * JSON.stringify({a: 1, b: 2})
1128
- * "{"a":1,"b":2}"
1129
- * JSON.stringify({b: 2, a: 1})
1130
- * "{"b":2,"a":1}"
1131
- * Modified from the apex implementation to sort arrays non-destructively.
1132
- * @param data Data to be JSON-stringified.
1133
- * @returns JSON.stringified value with consistent ordering of keys.
1134
- */
1135
- function stableJSONStringify(node) {
1136
- // This is for Date values.
1137
- if (node && node.toJSON && typeof node.toJSON === 'function') {
1138
- // eslint-disable-next-line no-param-reassign
1139
- node = node.toJSON();
1140
- }
1141
- if (node === undefined) {
1142
- return;
1143
- }
1144
- if (typeof node === 'number') {
1145
- return isFinite(node) ? '' + node : 'null';
1643
+ return combined;
1146
1644
  }
1147
- if (typeof node !== 'object') {
1148
- return stringify(node);
1645
+ isContextDependent(context, request) {
1646
+ return (request.config.recordIds &&
1647
+ (context.recordId === request.config.recordIds || // some may set this as string instead of array
1648
+ (request.config.recordIds.length === 1 &&
1649
+ request.config.recordIds[0] === context.recordId)));
1149
1650
  }
1150
- let i;
1151
- let out;
1152
- if (isArray(node)) {
1153
- // copy any array before sorting so we don't mutate the object.
1154
- // eslint-disable-next-line no-param-reassign
1155
- node = node.slice(0).sort();
1156
- out = '[';
1157
- for (i = 0; i < node.length; i++) {
1158
- if (i) {
1159
- out += ',';
1160
- }
1161
- out += stableJSONStringify(node[i]) || 'null';
1162
- }
1163
- return out + ']';
1651
+ }
1652
+
1653
+ class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
1654
+ constructor() {
1655
+ super(...arguments);
1656
+ this.adapterName = 'getObjectInfo';
1657
+ this.adapterFactory = getObjectInfoAdapterFactory;
1164
1658
  }
1165
- if (node === null) {
1166
- return 'null';
1659
+ buildConcreteRequest(similarRequest, context) {
1660
+ return {
1661
+ ...similarRequest,
1662
+ config: {
1663
+ ...similarRequest.config,
1664
+ objectApiName: context.objectApiName,
1665
+ },
1666
+ };
1167
1667
  }
1168
- const keys$1 = keys(node).sort();
1169
- out = '';
1170
- for (i = 0; i < keys$1.length; i++) {
1171
- const key = keys$1[i];
1172
- const value = stableJSONStringify(node[key]);
1173
- if (!value) {
1174
- continue;
1175
- }
1176
- if (out) {
1177
- out += ',';
1668
+ buildSaveRequestData(similarContext, context, request) {
1669
+ if (this.isContextDependent(context, request)) {
1670
+ return {
1671
+ request: this.transformForSave(request),
1672
+ context: similarContext,
1673
+ };
1178
1674
  }
1179
- out += stringify(key) + ':' + value;
1675
+ return {
1676
+ request: this.transformForSave(request),
1677
+ context,
1678
+ };
1679
+ }
1680
+ isContextDependent(context, request) {
1681
+ return (request.config.objectApiName !== undefined &&
1682
+ context.objectApiName === request.config.objectApiName);
1180
1683
  }
1181
- return '{' + out + '}';
1182
1684
  }
1183
- function isPromise(value) {
1184
- // check for Thenable due to test frameworks using custom Promise impls
1185
- return value !== null && value.then !== undefined;
1685
+
1686
+ class GetObjectInfosRequestStrategy extends LuvioAdapterRequestStrategy {
1687
+ constructor() {
1688
+ super(...arguments);
1689
+ this.adapterName = 'getObjectInfos';
1690
+ this.adapterFactory = getObjectInfosAdapterFactory;
1691
+ }
1692
+ buildConcreteRequest(similarRequest) {
1693
+ return similarRequest;
1694
+ }
1186
1695
  }
1187
1696
 
1188
- const APEX_ADAPTER_NAME = 'getApex';
1189
- const NORMALIZED_APEX_ADAPTER_NAME = `Apex.${APEX_ADAPTER_NAME}`;
1190
- const REFRESH_APEX_KEY = 'refreshApex';
1191
- const REFRESH_UIAPI_KEY = 'refreshUiApi';
1192
- const SUPPORTED_KEY = 'refreshSupported';
1193
- const UNSUPPORTED_KEY = 'refreshUnsupported';
1194
- const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
1195
- const REFRESH_EVENTTYPE = 'system';
1196
- const REFRESH_PAYLOAD_TARGET = 'adapters';
1197
- const REFRESH_PAYLOAD_SCOPE = 'lds';
1198
- const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
1199
- const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
1200
- const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
1201
- const NAMESPACE = 'lds';
1202
- const NETWORK_TRANSACTION_NAME = 'lds-network';
1203
- const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
1204
- // Aggregate Cache Stats and Metrics for all getApex invocations
1205
- const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
1206
- const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
1207
- // Observability (READS)
1208
- const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
1209
- const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
1210
- const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
1211
- class Instrumentation {
1697
+ const { keys: ObjectKeys$1 } = Object;
1698
+ const { isArray: ArrayIsArray, from: ArrayFrom } = Array;
1699
+ function isReduceAbleRelatedListConfig(config) {
1700
+ return config.relatedListsActionParameters.every((rlReq) => {
1701
+ return rlReq.relatedListId !== undefined && ObjectKeys$1(rlReq).length === 1;
1702
+ });
1703
+ }
1704
+ class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy {
1212
1705
  constructor() {
1213
- this.adapterUnfulfilledErrorCounters = {};
1214
- this.recordApiNameChangeCounters = {};
1215
- this.refreshAdapterEvents = {};
1216
- this.refreshApiCallEventStats = {
1217
- [REFRESH_APEX_KEY]: 0,
1218
- [REFRESH_UIAPI_KEY]: 0,
1219
- [SUPPORTED_KEY]: 0,
1220
- [UNSUPPORTED_KEY]: 0,
1706
+ super(...arguments);
1707
+ this.adapterName = 'getRelatedListsActions';
1708
+ this.adapterFactory = getRelatedListsActionsAdapterFactory;
1709
+ }
1710
+ buildConcreteRequest(similarRequest, context) {
1711
+ return {
1712
+ ...similarRequest,
1713
+ config: {
1714
+ ...similarRequest.config,
1715
+ recordIds: [context.recordId],
1716
+ },
1221
1717
  };
1222
- this.lastRefreshApiCall = null;
1223
- this.weakEtagZeroEvents = {};
1224
- this.adapterCacheMisses = new LRUCache(250);
1225
- if (typeof window !== 'undefined' && window.addEventListener) {
1226
- window.addEventListener('beforeunload', () => {
1227
- if (keys(this.weakEtagZeroEvents).length > 0) {
1228
- perfStart(NETWORK_TRANSACTION_NAME);
1229
- perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
1230
- }
1231
- });
1718
+ }
1719
+ buildSaveRequestData(similarContext, context, request) {
1720
+ if (this.isContextDependent(context, request)) {
1721
+ return {
1722
+ request: this.transformForSave({
1723
+ ...request,
1724
+ config: {
1725
+ ...request.config,
1726
+ recordIds: ['*'],
1727
+ },
1728
+ }),
1729
+ context: similarContext,
1730
+ };
1232
1731
  }
1233
- registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
1732
+ return {
1733
+ request: this.transformForSave(request),
1734
+ context,
1735
+ };
1736
+ }
1737
+ isContextDependent(context, request) {
1738
+ const isForContext = request.config.recordIds &&
1739
+ (context.recordId === request.config.recordIds || // some may set this as string instead of array
1740
+ (request.config.recordIds.length === 1 &&
1741
+ request.config.recordIds[0] === context.recordId));
1742
+ return isForContext && isReduceAbleRelatedListConfig(request.config);
1234
1743
  }
1235
1744
  /**
1236
- * Instruments an existing adapter to log argus metrics and cache stats.
1237
- * @param adapter The adapter function.
1238
- * @param metadata The adapter metadata.
1239
- * @param wireConfigKeyFn Optional function to transform wire configs to a unique key.
1240
- * @returns The wrapped adapter.
1745
+ * Can only reduce two requests when they have the same recordId, and
1746
+ * the individual relatedListAction config only have relatedListId.
1747
+ *
1748
+ * @param reqA
1749
+ * @param reqB
1750
+ * @returns boolean
1241
1751
  */
1242
- instrumentAdapter(adapter, metadata) {
1243
- // We are consolidating all apex adapter instrumentation calls under a single key
1244
- const { apiFamily, name, ttl } = metadata;
1245
- const adapterName = normalizeAdapterName(name, apiFamily);
1246
- const isGetApexAdapter = isApexAdapter(name);
1247
- const stats = isGetApexAdapter ? getApexCacheStats : registerLdsCacheStats(adapterName);
1248
- const ttlMissStats = isGetApexAdapter
1249
- ? getApexTtlCacheStats
1250
- : registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
1251
- /**
1252
- * W-8076905
1253
- * Dynamically generated metric. Simple counter for all requests made by this adapter.
1254
- */
1255
- const wireAdapterRequestMetric = isGetApexAdapter
1256
- ? getApexRequestCountMetric
1257
- : counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
1258
- const instrumentedAdapter = (config, requestContext) => {
1259
- // increment overall and adapter request metrics
1260
- wireAdapterRequestMetric.increment(1);
1261
- totalAdapterRequestSuccessMetric.increment(1);
1262
- // execute adapter logic
1263
- const result = adapter(config, requestContext);
1264
- // In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
1265
- // (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
1266
- //
1267
- // Note: we can't do a plain instanceof check for a promise here since the Promise may
1268
- // originate from another javascript realm (for example: in jest test). Instead we use a
1269
- // duck-typing approach by checking if the result has a then property.
1270
- //
1271
- // For adapters without persistent store:
1272
- // - total cache hit ratio:
1273
- // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
1274
- // For adapters with persistent store:
1275
- // - in-memory cache hit ratio:
1276
- // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
1277
- // - total cache hit ratio:
1278
- // ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
1279
- // if result === null then config is insufficient/invalid so do not log
1280
- if (isPromise(result)) {
1281
- stats.logMisses();
1282
- if (ttl !== undefined) {
1283
- this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
1284
- }
1285
- }
1286
- else if (result !== null) {
1287
- stats.logHits();
1288
- }
1289
- return result;
1290
- };
1291
- // Set the name property on the function for debugging purposes.
1292
- Object.defineProperty(instrumentedAdapter, 'name', {
1293
- value: name + '__instrumented',
1752
+ canCombine(reqA, reqB) {
1753
+ const [recordIdA, recordIdB] = [reqA.recordIds, reqB.recordIds].map((recordIds) => {
1754
+ return ArrayIsArray(recordIds)
1755
+ ? recordIds.length === 1
1756
+ ? recordIds[0]
1757
+ : null
1758
+ : recordIds;
1294
1759
  });
1295
- return instrumentAdapter(instrumentedAdapter, metadata);
1760
+ return (recordIdA === recordIdB &&
1761
+ recordIdA !== null &&
1762
+ isReduceAbleRelatedListConfig(reqA) &&
1763
+ isReduceAbleRelatedListConfig(reqB));
1764
+ }
1765
+ combineRequests(reqA, reqB) {
1766
+ const relatedListsIncluded = new Set();
1767
+ [reqA, reqB].forEach(({ relatedListsActionParameters }) => {
1768
+ relatedListsActionParameters.forEach(({ relatedListId }) => relatedListsIncluded.add(relatedListId));
1769
+ });
1770
+ return {
1771
+ recordIds: reqA.recordIds,
1772
+ relatedListsActionParameters: ArrayFrom(relatedListsIncluded).map((relatedListId) => ({
1773
+ relatedListId,
1774
+ })),
1775
+ };
1776
+ }
1777
+ }
1778
+
1779
+ class GetRelatedListInfoBatchRequestStrategy extends LuvioAdapterRequestStrategy {
1780
+ constructor() {
1781
+ super(...arguments);
1782
+ this.adapterName = 'getRelatedListInfoBatch';
1783
+ this.adapterFactory = getRelatedListInfoBatchAdapterFactory;
1296
1784
  }
1297
- /**
1298
- * 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.
1299
- * Backed by an LRU Cache implementation to prevent too many record entries from being stored in-memory.
1300
- * @param name The wire adapter name.
1301
- * @param config The config passed into wire adapter.
1302
- * @param ttlMissStats CacheStatsLogger to log misses out of TTL.
1303
- * @param currentCacheMissTimestamp Timestamp for when the request was made.
1304
- * @param ttl TTL for the wire adapter.
1305
- */
1306
- logAdapterCacheMissOutOfTtlDuration(name, config, ttlMissStats, currentCacheMissTimestamp, ttl) {
1307
- const configKey = `${name}:${stableJSONStringify(config)}`;
1308
- const existingCacheMissTimestamp = this.adapterCacheMisses.get(configKey);
1309
- this.adapterCacheMisses.set(configKey, currentCacheMissTimestamp);
1310
- if (existingCacheMissTimestamp !== undefined) {
1311
- const duration = currentCacheMissTimestamp - existingCacheMissTimestamp;
1312
- if (duration > ttl) {
1313
- ttlMissStats.logMisses();
1314
- }
1315
- }
1785
+ buildConcreteRequest(similarRequest, _context) {
1786
+ return similarRequest;
1316
1787
  }
1317
- /**
1318
- * Injected to LDS for Luvio specific instrumentation.
1319
- *
1320
- * @param context The transaction context.
1321
- */
1322
- instrumentLuvio(context) {
1323
- instrumentLuvio(context);
1324
- if (this.isRefreshAdapterEvent(context)) {
1325
- this.aggregateRefreshAdapterEvents(context);
1326
- }
1327
- else if (this.isAdapterUnfulfilledError(context)) {
1328
- this.incrementAdapterRequestErrorCount(context);
1329
- }
1330
- else ;
1788
+ canCombine(reqA, reqB) {
1789
+ return reqA.parentObjectApiName === reqB.parentObjectApiName;
1331
1790
  }
1332
- /**
1333
- * Returns whether or not this is a RefreshAdapterEvent.
1334
- * @param context The transaction context.
1335
- * @returns Whether or not this is a RefreshAdapterEvent.
1336
- */
1337
- isRefreshAdapterEvent(context) {
1338
- return context[REFRESH_ADAPTER_EVENT] === true;
1791
+ combineRequests(reqA, reqB) {
1792
+ const combined = { ...reqA };
1793
+ combined.relatedListNames = Array.from(new Set([...reqA.relatedListNames, ...reqB.relatedListNames]));
1794
+ return combined;
1339
1795
  }
1340
- /**
1341
- * Returns whether or not this is an AdapterUnfulfilledError.
1342
- * @param context The transaction context.
1343
- * @returns Whether or not this is an AdapterUnfulfilledError.
1344
- */
1345
- isAdapterUnfulfilledError(context) {
1346
- return context[ADAPTER_UNFULFILLED_ERROR] === true;
1796
+ }
1797
+
1798
+ const GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME = 'getRelatedListRecordsBatch';
1799
+ class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrategy {
1800
+ constructor() {
1801
+ super(...arguments);
1802
+ this.adapterName = GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME;
1803
+ this.adapterFactory = getRelatedListRecordsBatchAdapterFactory;
1347
1804
  }
1348
- /**
1349
- * Specific instrumentation for getRecordNotifyChange.
1350
- * temporary implementation to match existing aura call for now
1351
- *
1352
- * @param uniqueWeakEtags whether weakEtags match or not
1353
- * @param error if dispatchResourceRequest fails for any reason
1354
- */
1355
- notifyChangeNetwork(uniqueWeakEtags, error) {
1356
- perfStart(NETWORK_TRANSACTION_NAME);
1357
- if (error === true) {
1358
- perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
1359
- }
1360
- else {
1361
- perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
1362
- }
1805
+ buildConcreteRequest(similarRequest, context) {
1806
+ return {
1807
+ ...similarRequest,
1808
+ config: {
1809
+ ...similarRequest.config,
1810
+ parentRecordId: context.recordId,
1811
+ },
1812
+ };
1363
1813
  }
1364
- /**
1365
- * Parses and aggregates weakETagZero events to be sent in summarized log line.
1366
- * @param context The transaction context.
1367
- */
1368
- aggregateWeakETagEvents(incomingWeakEtagZero, existingWeakEtagZero, apiName) {
1369
- const key = 'weaketag-0-' + apiName;
1370
- if (this.weakEtagZeroEvents[key] === undefined) {
1371
- this.weakEtagZeroEvents[key] = {
1372
- [EXISTING_WEAKETAG_0_KEY]: 0,
1373
- [INCOMING_WEAKETAG_0_KEY]: 0,
1814
+ buildSaveRequestData(similarContext, context, request) {
1815
+ if (this.isContextDependent(context, request)) {
1816
+ return {
1817
+ request: this.transformForSave({
1818
+ ...request,
1819
+ config: {
1820
+ ...request.config,
1821
+ parentRecordId: '*',
1822
+ },
1823
+ }),
1824
+ context: similarContext,
1374
1825
  };
1375
1826
  }
1376
- if (existingWeakEtagZero) {
1377
- this.weakEtagZeroEvents[key][EXISTING_WEAKETAG_0_KEY] += 1;
1378
- }
1379
- if (incomingWeakEtagZero) {
1380
- this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
1381
- }
1827
+ return {
1828
+ request: this.transformForSave(request),
1829
+ context,
1830
+ };
1382
1831
  }
1383
- /**
1384
- * Aggregates refresh adapter events to be sent in summarized log line.
1385
- * - how many times refreshApex is called
1386
- * - how many times refresh from lightning/uiRecordApi is called
1387
- * - number of supported calls: refreshApex called on apex adapter
1388
- * - number of unsupported calls: refreshApex on non-apex adapter
1389
- * + any use of refresh from uiRecordApi module
1390
- * - count of refresh calls per adapter
1391
- * @param context The refresh adapter event.
1392
- */
1393
- aggregateRefreshAdapterEvents(context) {
1394
- // We are consolidating all apex adapter instrumentation calls under a single key
1395
- // Adding additional logging that getApex adapters can invoke? Read normalizeAdapterName ts-doc.
1396
- const adapterName = normalizeAdapterName(context.adapterName);
1397
- if (this.lastRefreshApiCall === REFRESH_APEX_KEY) {
1398
- if (isApexAdapter(adapterName)) {
1399
- this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
1400
- }
1401
- else {
1402
- this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
1403
- }
1404
- }
1405
- else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
1406
- this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
1407
- }
1408
- if (this.refreshAdapterEvents[adapterName] === undefined) {
1409
- this.refreshAdapterEvents[adapterName] = 0;
1410
- }
1411
- this.refreshAdapterEvents[adapterName] += 1;
1412
- this.lastRefreshApiCall = null;
1832
+ isContextDependent(context, request) {
1833
+ return context.recordId === request.config.parentRecordId;
1413
1834
  }
1414
1835
  /**
1415
- * Increments call stat for incoming refresh api call, and sets the name
1416
- * to be used in {@link aggregateRefreshCalls}
1417
- * @param from The name of the refresh function called.
1836
+ * Can combine two seperate batch requests if the parentRecordId is the same.
1837
+ * @param reqA The first GetRelatedListRecordsBatchConfig.
1838
+ * @param reqB The first GetRelatedListRecordsBatchConfig.
1839
+ * @returns true if the requests can be combined, otherwise false.
1418
1840
  */
1419
- handleRefreshApiCall(apiName) {
1420
- this.refreshApiCallEventStats[apiName] += 1;
1421
- // set function call to be used with aggregateRefreshCalls
1422
- this.lastRefreshApiCall = apiName;
1841
+ canCombine(reqA, reqB) {
1842
+ return reqA.parentRecordId === reqB.parentRecordId;
1423
1843
  }
1424
1844
  /**
1425
- * W-7302241
1426
- * Logs refresh call summary stats as a LightningInteraction.
1845
+ * Merge the relatedListParameters together between two combinable batch requests.
1846
+ * @param reqA The first GetRelatedListRecordsBatchConfig.
1847
+ * @param reqB The first GetRelatedListRecordsBatchConfig.
1848
+ * @returns The combined request.
1427
1849
  */
1428
- logRefreshStats() {
1429
- if (keys(this.refreshAdapterEvents).length > 0) {
1430
- interaction(REFRESH_PAYLOAD_TARGET, REFRESH_PAYLOAD_SCOPE, this.refreshAdapterEvents, REFRESH_EVENTSOURCE, REFRESH_EVENTTYPE, this.refreshApiCallEventStats);
1431
- this.resetRefreshStats();
1432
- }
1850
+ combineRequests(reqA, reqB) {
1851
+ const relatedListParametersMap = new Set(reqA.relatedListParameters.map((relatedListParameter) => {
1852
+ return stableJSONStringify(relatedListParameter);
1853
+ }));
1854
+ const reqBRelatedListParametersToAdd = reqB.relatedListParameters.filter((relatedListParameter) => {
1855
+ return !relatedListParametersMap.has(stableJSONStringify(relatedListParameter));
1856
+ });
1857
+ reqA.relatedListParameters = reqA.relatedListParameters.concat(reqBRelatedListParametersToAdd);
1858
+ return reqA;
1433
1859
  }
1434
- /**
1435
- * Resets the stat trackers for refresh call events.
1436
- */
1437
- resetRefreshStats() {
1438
- this.refreshAdapterEvents = {};
1439
- this.refreshApiCallEventStats = {
1440
- [REFRESH_APEX_KEY]: 0,
1441
- [REFRESH_UIAPI_KEY]: 0,
1442
- [SUPPORTED_KEY]: 0,
1443
- [UNSUPPORTED_KEY]: 0,
1860
+ }
1861
+
1862
+ class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
1863
+ constructor() {
1864
+ super(...arguments);
1865
+ this.adapterName = 'getRelatedListRecords';
1866
+ this.adapterFactory = getRelatedListRecordsAdapterFactory;
1867
+ }
1868
+ buildConcreteRequest(similarRequest, context) {
1869
+ return {
1870
+ ...similarRequest,
1871
+ config: {
1872
+ ...similarRequest.config,
1873
+ parentRecordId: context.recordId,
1874
+ },
1444
1875
  };
1445
- this.lastRefreshApiCall = null;
1446
1876
  }
1447
1877
  /**
1448
- * W-7801618
1449
- * Counter for occurrences where the incoming record to be merged has a different apiName.
1450
- * Dynamically generated metric, stored in an {@link RecordApiNameChangeCounters} object.
1451
1878
  *
1452
- * @param context The transaction context.
1879
+ * This method returns GetRelatedListRecordsRequest[] that won't be part of a batch request.
1453
1880
  *
1454
- * Note: Short-lived metric candidate, remove at the end of 230
1881
+ * ADG currently handles the batching of GetRelatedListRecords -> GetRelatedListRecordsBatch
1882
+ * https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListRecordsBatchReducer.js
1883
+ *
1884
+ * For performance reasons (fear to overfetch), we only check that the Single relatedListId is not present in any of the Batch requests,
1885
+ * but we don't check for any other parameters.
1886
+ *
1887
+ * @param unfilteredRequests All of the request available for predictions.
1888
+ * @returns GetRelatedListRecordsRequest[] That should be a prediction.
1455
1889
  */
1456
- incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
1457
- let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
1458
- if (apiNameChangeCounter === undefined) {
1459
- apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
1460
- this.recordApiNameChangeCounters[existingApiName] = apiNameChangeCounter;
1890
+ reduce(unfilteredRequests) {
1891
+ // Batch requests by [parentRecordId]->[RelatedListIds]
1892
+ const batchRequests = unfilteredRequests.filter((request) => request.adapterName === GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME).reduce((acc, request) => {
1893
+ // required properties, enforced by adapter typecheck
1894
+ const { parentRecordId, relatedListParameters } = request.config;
1895
+ const existingRlSet = acc.get(parentRecordId) || new Set();
1896
+ // relatedListId enforced by adapter typecheck
1897
+ relatedListParameters.forEach((rlParam) => existingRlSet.add(rlParam.relatedListId));
1898
+ acc.set(parentRecordId, existingRlSet);
1899
+ return acc;
1900
+ }, new Map());
1901
+ const singleRequests = unfilteredRequests.filter((request) => request.adapterName === this.adapterName);
1902
+ return singleRequests.filter((request) => {
1903
+ // required props enforced by adapter typecheck
1904
+ const { parentRecordId, relatedListId } = request.config;
1905
+ const batchForParentRecordId = batchRequests.get(parentRecordId);
1906
+ return !(batchForParentRecordId && batchForParentRecordId.has(relatedListId));
1907
+ });
1908
+ }
1909
+ buildSaveRequestData(similarContext, context, request) {
1910
+ if (this.isContextDependent(context, request)) {
1911
+ return {
1912
+ request: this.transformForSave({
1913
+ ...request,
1914
+ config: {
1915
+ ...request.config,
1916
+ parentRecordId: '*',
1917
+ },
1918
+ }),
1919
+ context: similarContext,
1920
+ };
1461
1921
  }
1462
- apiNameChangeCounter.increment(1);
1922
+ return {
1923
+ request: this.transformForSave(request),
1924
+ context,
1925
+ };
1926
+ }
1927
+ isContextDependent(context, request) {
1928
+ return context.recordId === request.config.parentRecordId;
1929
+ }
1930
+ }
1931
+
1932
+ class LexRequestRunner {
1933
+ constructor(luvio) {
1934
+ this.luvio = luvio;
1935
+ this.requestStrategies = {
1936
+ getRecord: new GetRecordRequestStrategy(),
1937
+ getRecords: new GetRecordsRequestStrategy(),
1938
+ getRecordActions: new GetRecordActionsRequestStrategy(),
1939
+ getRecordAvatars: new GetRecordAvatarsRequestStrategy(),
1940
+ getObjectInfo: new GetObjectInfoRequestStrategy(),
1941
+ getObjectInfos: new GetObjectInfosRequestStrategy(),
1942
+ getRelatedListsActions: new GetRelatedListsActionsRequestStrategy(),
1943
+ getRelatedListInfoBatch: new GetRelatedListInfoBatchRequestStrategy(),
1944
+ getRelatedListRecords: new GetRelatedListRecordsRequestStrategy(),
1945
+ getRelatedListRecordsBatch: new GetRelatedListRecordsBatchRequestStrategy(),
1946
+ };
1463
1947
  }
1464
- /**
1465
- * W-8620679
1466
- * Increment the counter for an UnfulfilledSnapshotError coming from luvio
1467
- *
1468
- * @param context The transaction context.
1469
- */
1470
- incrementAdapterRequestErrorCount(context) {
1471
- // We are consolidating all apex adapter instrumentation calls under a single key
1472
- const adapterName = normalizeAdapterName(context.adapterName);
1473
- let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
1474
- if (adapterRequestErrorCounter === undefined) {
1475
- adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
1476
- this.adapterUnfulfilledErrorCounters[adapterName] = adapterRequestErrorCounter;
1948
+ reduceRequests(requests) {
1949
+ return Object.values(this.requestStrategies)
1950
+ .map((strategy) => strategy.reduce(requests))
1951
+ .flat();
1952
+ }
1953
+ runRequest(request) {
1954
+ if (request.adapterName in this.requestStrategies) {
1955
+ const adapterFactory = this.requestStrategies[request.adapterName].adapterFactory;
1956
+ return Promise.resolve(adapterFactory(this.luvio)(request.config)).then();
1477
1957
  }
1478
- adapterRequestErrorCounter.increment(1);
1479
- totalAdapterErrorMetric.increment(1);
1958
+ return Promise.resolve(undefined);
1480
1959
  }
1481
1960
  }
1482
- function createMetricsKey(owner, name, unit) {
1483
- let metricName = name;
1484
- if (unit) {
1485
- metricName = metricName + '.' + unit;
1961
+
1962
+ class InMemoryPrefetchStorage {
1963
+ constructor() {
1964
+ this.data = {};
1486
1965
  }
1487
- return {
1488
- get() {
1489
- return { owner: owner, name: metricName };
1490
- },
1491
- };
1492
- }
1493
- /**
1494
- * Returns whether adapter is an Apex one or not.
1495
- * @param adapterName The name of the adapter.
1496
- */
1497
- function isApexAdapter(adapterName) {
1498
- return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
1499
- }
1500
- /**
1501
- * Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
1502
- * API family, if supplied. Example: `UiApi.getRecord`.
1503
- *
1504
- * Note: If you are adding additional logging that can come from getApex adapter contexts that provide
1505
- * the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
1506
- * ensure to call this method to normalize all logging to 'getApex'. This
1507
- * is because Argus has a 50k key cardinality limit. More context: W-8379680.
1508
- *
1509
- * @param adapterName The name of the adapter.
1510
- * @param apiFamily The API family of the adapter.
1511
- */
1512
- function normalizeAdapterName(adapterName, apiFamily) {
1513
- if (isApexAdapter(adapterName)) {
1514
- return NORMALIZED_APEX_ADAPTER_NAME;
1966
+ set(key, value) {
1967
+ this.data[key] = value;
1968
+ return Promise.resolve();
1515
1969
  }
1516
- return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
1517
- }
1518
- const timerMetricTracker = create(null);
1519
- /**
1520
- * Calls instrumentation/service telemetry timer
1521
- * @param name Name of the metric
1522
- * @param duration number to update backing percentile histogram, negative numbers ignored
1523
- */
1524
- function updateTimerMetric(name, duration) {
1525
- let metric = timerMetricTracker[name];
1526
- if (metric === undefined) {
1527
- metric = timer(createMetricsKey(NAMESPACE, name));
1528
- timerMetricTracker[name] = metric;
1970
+ get(key) {
1971
+ return this.data[key];
1529
1972
  }
1530
- timerMetricAddDuration(metric, duration);
1531
1973
  }
1532
- function timerMetricAddDuration(timer, duration) {
1533
- // Guard against negative values since it causes error to be thrown by MetricsService
1534
- if (duration >= 0) {
1535
- timer.addDuration(duration);
1974
+
1975
+ const { keys: ObjectKeys } = Object;
1976
+ const DEFAULT_STORAGE_OPTIONS = {
1977
+ name: 'ldsPredictiveLoading',
1978
+ persistent: true,
1979
+ secure: true,
1980
+ maxSize: 7 * 1024 * 1024,
1981
+ expiration: 12 * 60 * 60,
1982
+ clearOnInit: false,
1983
+ debugLogging: false,
1984
+ version: 3,
1985
+ };
1986
+ function buildAuraPrefetchStorage(options = {}) {
1987
+ const auraStorage = createStorage({
1988
+ ...DEFAULT_STORAGE_OPTIONS,
1989
+ ...options,
1990
+ });
1991
+ const inMemoryStorage = new InMemoryPrefetchStorage();
1992
+ if (auraStorage === null) {
1993
+ return inMemoryStorage;
1536
1994
  }
1995
+ return new AuraPrefetchStorage(auraStorage, inMemoryStorage);
1537
1996
  }
1538
- /**
1539
- * W-10315098
1540
- * Increments the counter associated with the request response. Counts are bucketed by status.
1541
- */
1542
- const requestResponseMetricTracker = create(null);
1543
- function incrementRequestResponseCount(cb) {
1544
- const status = cb().status;
1545
- let metric = requestResponseMetricTracker[status];
1546
- if (metric === undefined) {
1547
- metric = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, NETWORK_ADAPTER_RESPONSE_METRIC_NAME, `${status.valueOf()}`));
1548
- requestResponseMetricTracker[status] = metric;
1997
+ class AuraPrefetchStorage {
1998
+ constructor(auraStorage, inMemoryStorage) {
1999
+ this.auraStorage = auraStorage;
2000
+ this.inMemoryStorage = inMemoryStorage;
2001
+ /**
2002
+ * Because of aura is an event loop hog and we therefore need to minimize asynchronicity in LEX,
2003
+ * we need need to preload predictions and treat read operations sync. Not making it sync, will cause
2004
+ * some request to be sent the network when they could be dedupe against those from the predictions.
2005
+ *
2006
+ * Drawbacks of this approach:
2007
+ * 1. Loading all of aura storage into memory and then updating it based on changes to that in memory
2008
+ * representation means that updates to predictions in aura storage across different tabs will result
2009
+ * in overwrites, not graceful merges of predictions.
2010
+ * 2. If whoever is consuming this tries to get and run predictions before this is done loading,
2011
+ * then they will (potentially incorrectly) think that we don't have any predictions.
2012
+ */
2013
+ auraStorage.getAll().then((results) => {
2014
+ ObjectKeys(results).forEach((key) => this.inMemoryStorage.set(key, results[key]));
2015
+ });
1549
2016
  }
1550
- metric.increment();
1551
- }
1552
- function logObjectInfoChanged() {
1553
- logObjectInfoChanged$1();
1554
- }
1555
- /**
1556
- * Create a new instrumentation cache stats and return it.
1557
- *
1558
- * @param name The cache logger name.
1559
- */
1560
- function registerLdsCacheStats(name) {
1561
- return registerCacheStats(`${NAMESPACE}:${name}`);
1562
- }
1563
- /**
1564
- * Add or overwrite hooks that require aura implementations
1565
- */
1566
- function setAuraInstrumentationHooks() {
1567
- instrument({
1568
- recordConflictsResolved: (serverRequestCount) => {
1569
- // Ignore 0 values which can originate from ADS bridge
1570
- if (serverRequestCount > 0) {
1571
- updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
1572
- }
1573
- },
1574
- nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
1575
- const metricName = `merge-null-dv-count.${fieldType}`;
1576
- if (fieldType === 'scalar') {
1577
- incrementCounterMetric(`${metricName}.${areValuesEqual}`);
1578
- }
1579
- else {
1580
- incrementCounterMetric(metricName);
2017
+ set(key, value) {
2018
+ const inMemoryResult = this.inMemoryStorage.set(key, value);
2019
+ this.auraStorage.set(key, value).catch((error) => {
2020
+ if (process.env.NODE_ENV !== 'production') {
2021
+ // eslint-disable-next-line no-console
2022
+ console.error('Error save LDS prediction: ', error);
1581
2023
  }
1582
- },
1583
- getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
1584
- getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
1585
- notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
1586
- notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
1587
- recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
1588
- weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
1589
- getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
1590
- });
1591
- withRegistration('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
1592
- instrument$1({
1593
- logCrud: logCRUDLightningInteraction,
1594
- networkResponse: incrementRequestResponseCount,
1595
- });
1596
- instrument$2({
1597
- refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
1598
- instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
1599
- });
1600
- instrument$3({
1601
- timerMetricAddDuration: updateTimerMetric,
1602
- });
1603
- // Our getRecord through aggregate-ui CRUD logging has moved
1604
- // to lds-network-adapter. We still need to respect the
1605
- // orgs environment setting
1606
- if (forceRecordTransactionsDisabled === false) {
1607
- ldsNetworkAdapterInstrument({
1608
- getRecordAggregateResolve: (cb) => {
1609
- const { recordId, apiName } = cb();
1610
- logCRUDLightningInteraction('read', {
1611
- recordId,
1612
- recordType: apiName,
1613
- state: 'SUCCESS',
1614
- });
1615
- },
1616
- getRecordAggregateReject: (cb) => {
1617
- const recordId = cb();
1618
- logCRUDLightningInteraction('read', {
1619
- recordId,
1620
- state: 'ERROR',
1621
- });
1622
- },
1623
2024
  });
2025
+ return inMemoryResult;
2026
+ }
2027
+ get(key) {
2028
+ // we never read from the AuraStorage, except in construction.
2029
+ return this.inMemoryStorage.get(key);
1624
2030
  }
1625
- withRegistration('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
1626
- }
1627
- /**
1628
- * Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
1629
- *
1630
- * @param luvio The Luvio instance to instrument.
1631
- * @param store The InMemoryStore to instrument.
1632
- */
1633
- function setupInstrumentation(luvio, store) {
1634
- setupInstrumentation$1(luvio, store);
1635
- setAuraInstrumentationHooks();
1636
- }
1637
- /**
1638
- * Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
1639
- * disable all crud operations if it is on.
1640
- * @param eventSource - Source of the logging event.
1641
- * @param attributes - Free form object of attributes to log.
1642
- */
1643
- function logCRUDLightningInteraction(eventSource, attributes) {
1644
- interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
1645
2031
  }
1646
- const instrumentation = new Instrumentation();
1647
2032
 
1648
2033
  class NoComposedAdapterTypeError extends TypeError {
1649
2034
  constructor(message, resourceRequest) {
@@ -1817,33 +2202,6 @@ function setupPredictivePrefetcher(luvio) {
1817
2202
  registerPrefetcher(luvio, prefetcher);
1818
2203
  __lexPrefetcher = prefetcher;
1819
2204
  }
1820
- /**
1821
- * @deprecated This function is deprecated in favor of `buildPredictorForContext`.
1822
- * We only keep it so if the lds.usePredictiveLoading gate is open the existing functionality
1823
- * on LASR don't break.
1824
- * Note: we don't want to make it a noop either because then the gate toggle will be meaningless.
1825
- */
1826
- async function predictiveLoadPage(preloadProps, runPredictions) {
1827
- // the gate is disabled and the prefetcher was not setup.
1828
- if (__lexPrefetcher === undefined) {
1829
- return;
1830
- }
1831
- // This chunk configures which page we're going to use to try and preload.
1832
- const { objectApiName } = preloadProps.context;
1833
- const { recordId, actionName } = preloadProps.pageReference.attributes;
1834
- __lexPrefetcher.context = {
1835
- objectApiName,
1836
- recordId,
1837
- actionName,
1838
- type: 'recordPage',
1839
- };
1840
- // This chunk tells the prefetcher to receive events, send off any predictions we have from previous loads, then setup idle detection to stop predicting.
1841
- __lexPrefetcher.startRecording();
1842
- onIdleDetected(() => {
1843
- __lexPrefetcher.stopRecording();
1844
- });
1845
- return runPredictions ? __lexPrefetcher.predict() : Promise.resolve();
1846
- }
1847
2205
  /**
1848
2206
  * @typedef {Object} RecordHomePageContext
1849
2207
  * @property {string} objectApiName - The API name of the object.
@@ -1860,12 +2218,14 @@ async function predictiveLoadPage(preloadProps, runPredictions) {
1860
2218
  * @returns {{
1861
2219
  * hasPredictions: () => boolean,
1862
2220
  * watchPageLoadForPredictions: () => void,
1863
- * runPredictions: () => ReturnType<Promise<void>>
2221
+ * runPredictions: () => ReturnType<Promise<void>>,
2222
+ * getPredictionSummary: () => Map<String,Number>
1864
2223
  * }} An object containing methods to handle predictions:
1865
2224
  * - hasPredictions: Checks if there are any predictions available.
1866
2225
  * - watchPageLoadForPredictions: Starts recording the page request for prediction purposes.
1867
2226
  * - runPredictions: Executes the predictions based on the recorded data and returns a promise
1868
2227
  * of when those predictions are completed.
2228
+ * - getPredictionSummary: Returns the count of exact and similar prediction requests
1869
2229
  */
1870
2230
  function buildPredictorForContext(context) {
1871
2231
  // the gate is disabled and the prefetcher was not setup.
@@ -1886,7 +2246,10 @@ function buildPredictorForContext(context) {
1886
2246
  });
1887
2247
  },
1888
2248
  runPredictions() {
1889
- return __lexPrefetcher.predict();
2249
+ return executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_PREDICT, (_act) => __lexPrefetcher.predict());
2250
+ },
2251
+ getPredictionSummary() {
2252
+ return __lexPrefetcher.getPredictionSummary();
1890
2253
  },
1891
2254
  };
1892
2255
  }
@@ -1895,7 +2258,7 @@ function initializeLDS() {
1895
2258
  const storeOptions = {
1896
2259
  scheduler: () => { },
1897
2260
  };
1898
- const store = new InMemoryStore(storeOptions);
2261
+ const store = new InMemoryStore$1(storeOptions);
1899
2262
  const environment = new Environment(store, composedNetworkAdapter);
1900
2263
  const luvio = new Luvio(environment, {
1901
2264
  instrument: instrumentation.instrumentLuvio.bind(instrumentation),
@@ -1909,11 +2272,46 @@ function initializeLDS() {
1909
2272
  setupPredictivePrefetcher(luvio);
1910
2273
  }
1911
2274
  }
2275
+ // Initializes OneStore in LEX
2276
+ function initializeOneStore() {
2277
+ // Build default set of services
2278
+ const services = {
2279
+ ...buildDefaultKeySubscriptionService(),
2280
+ ...buildInMemoryMetadataRepositoryService(),
2281
+ ...buildInMemoryStoreService(),
2282
+ ...buildDefaultTypeRegistryService(),
2283
+ ...buildAuraNetworkService(),
2284
+ };
2285
+ const serviceVersions = {
2286
+ keySubscription: "1.0" /* KeySubscriptionServiceInfo.VERSION */,
2287
+ metadataRepository: "1.0" /* MetadataRepositoryServiceInfo.VERSION */,
2288
+ store: "1.0" /* StoreServiceInfo.VERSION */,
2289
+ typeRegistry: "1.0" /* TypeRegistryServiceInfo.VERSION */,
2290
+ auraNetwork: "1.0" /* AuraNetworkServiceInfo.VERSION */,
2291
+ };
2292
+ withRegistration('commandModule', (registration) => {
2293
+ const matchingServices = {};
2294
+ if (Object.entries(registration.services).every(([service, version]) => service in services &&
2295
+ satisfies(serviceVersions[service], version) &&
2296
+ (matchingServices[service] = services[service]))) {
2297
+ registration.setServices(matchingServices);
2298
+ }
2299
+ });
2300
+ }
2301
+ function buildAuraNetworkService() {
2302
+ const auraNetwork = {
2303
+ auraNetwork: executeGlobalControllerRawResponse,
2304
+ };
2305
+ return auraNetwork;
2306
+ }
1912
2307
  // service function to be invoked by Aura
1913
2308
  function ldsEngineCreator() {
1914
2309
  initializeLDS();
2310
+ if (oneStoreEnabled.isOpen({ fallback: false })) {
2311
+ initializeOneStore();
2312
+ }
1915
2313
  return { name: 'ldsEngineCreator' };
1916
2314
  }
1917
2315
 
1918
- export { buildPredictorForContext, ldsEngineCreator as default, initializeLDS, predictiveLoadPage };
1919
- // version: 1.278.0-f0e8ebcd6
2316
+ export { buildPredictorForContext, ldsEngineCreator as default, initializeLDS, initializeOneStore };
2317
+ // version: 1.280.0-92c104b03