@salesforce/lds-runtime-aura 1.279.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.
@@ -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, getRelatedListRecordsBatchAdapterFactory, getRelatedListRecordsAdapterFactory, 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,126 +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
- getPredictionSummary() {
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 { exact: exactPageRequests.length, similar: similarPageRequests.length };
190
- }
191
- hasPredictions() {
192
- const summary = this.getPredictionSummary();
193
- return summary.exact > 0 || summary.similar > 0;
194
- }
195
- getSimilarPageRequests() {
196
- let resolvedSimilarPageRequests = [];
197
- if (this.page.similarContext !== undefined) {
198
- const similarPageRequests = this.repository.getPageRequests(this.page.similarContext);
199
- if (similarPageRequests !== undefined) {
200
- resolvedSimilarPageRequests = similarPageRequests.map((request) => this.page.resolveSimilarRequest(request));
201
- }
202
- }
203
- return resolvedSimilarPageRequests;
204
- }
205
- }
206
-
207
- class LexPredictivePrefetcher extends ApplicationPredictivePrefetcher {
208
- constructor(context, repository, requestRunner,
209
- // These strategies need to be in sync with the "predictiveDataLoadCapable" list
210
- // from scripts/lds-uiapi-plugin.js
211
- requestStrategies) {
212
- super(context, repository, requestRunner);
213
- this.requestStrategies = requestStrategies;
214
- this.page = this.getPage();
215
- }
216
- getPage() {
217
- if (RecordHomePage.handlesContext(this.context)) {
218
- return new RecordHomePage(this.context, this.requestStrategies);
219
- }
220
- return new LexDefaultPage(this.context);
221
- }
222
- }
223
-
224
- // Copy-pasted from adapter-utils. This util should be extracted from generated code and imported in prefetch repository.
225
- const { keys: ObjectKeys$2 } = Object;
226
- const { stringify: JSONStringify } = JSON;
227
- const { isArray: ArrayIsArray$1 } = Array;
228
- /**
229
- * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
230
- * This is needed because insertion order for JSON.stringify(object) affects output:
231
- * JSON.stringify({a: 1, b: 2})
232
- * "{"a":1,"b":2}"
233
- * JSON.stringify({b: 2, a: 1})
234
- * "{"b":2,"a":1}"
235
- * @param data Data to be JSON-stringified.
236
- * @returns JSON.stringified value with consistent ordering of keys.
237
- */
238
- function stableJSONStringify$1(node) {
239
- // This is for Date values.
240
- if (node && node.toJSON && typeof node.toJSON === 'function') {
241
- // eslint-disable-next-line no-param-reassign
242
- node = node.toJSON();
243
- }
244
- if (node === undefined) {
245
- return;
246
- }
247
- if (typeof node === 'number') {
248
- return isFinite(node) ? '' + node : 'null';
249
- }
250
- if (typeof node !== 'object') {
251
- return JSONStringify(node);
553
+ if (typeof node !== 'object') {
554
+ return stringify(node);
252
555
  }
253
556
  let i;
254
557
  let out;
255
- 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();
256
562
  out = '[';
257
563
  for (i = 0; i < node.length; i++) {
258
564
  if (i) {
@@ -265,10 +571,10 @@ function stableJSONStringify$1(node) {
265
571
  if (node === null) {
266
572
  return 'null';
267
573
  }
268
- const keys = ObjectKeys$2(node).sort();
574
+ const keys$1 = keys(node).sort();
269
575
  out = '';
270
- for (i = 0; i < keys.length; i++) {
271
- const key = keys[i];
576
+ for (i = 0; i < keys$1.length; i++) {
577
+ const key = keys$1[i];
272
578
  const value = stableJSONStringify$1(node[key]);
273
579
  if (!value) {
274
580
  continue;
@@ -276,716 +582,843 @@ function stableJSONStringify$1(node) {
276
582
  if (out) {
277
583
  out += ',';
278
584
  }
279
- out += JSONStringify(key) + ':' + value;
585
+ out += stringify(key) + ':' + value;
280
586
  }
281
587
  return '{' + out + '}';
282
588
  }
283
- function isObject(obj) {
284
- return obj !== null && typeof obj === 'object';
285
- }
286
- function deepEquals(objA, objB) {
287
- if (objA === objB)
288
- return true;
289
- if (objA instanceof Date && objB instanceof Date)
290
- return objA.getTime() === objB.getTime();
291
- // If one of them is not an object, they are not deeply equal
292
- if (!isObject(objA) || !isObject(objB))
293
- return false;
294
- // Filter out keys set as undefined, we can compare undefined as equals.
295
- const keysA = ObjectKeys$2(objA).filter((key) => objA[key] !== undefined);
296
- const keysB = ObjectKeys$2(objB).filter((key) => objB[key] !== undefined);
297
- // If the objects do not have the same set of keys, they are not deeply equal
298
- if (keysA.length !== keysB.length)
299
- return false;
300
- for (const key of keysA) {
301
- const valA = objA[key];
302
- const valB = objB[key];
303
- const areObjects = isObject(valA) && isObject(valB);
304
- // If both values are objects, recursively compare them
305
- if (areObjects && !deepEquals(valA, valB))
306
- return false;
307
- // If only one value is an object or if the values are not strictly equal, they are not deeply equal
308
- if (!areObjects && valA !== valB)
309
- return false;
310
- }
311
- 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;
312
592
  }
313
593
 
314
- class PrefetchRepository {
315
- constructor(storage) {
316
- this.storage = storage;
317
- this.requestBuffer = new Map();
318
- }
319
- clearRequestBuffer() {
320
- this.requestBuffer.clear();
321
- }
322
- async flushRequestsToStorage() {
323
- const setPromises = [];
324
- for (const [id, batch] of this.requestBuffer) {
325
- const page = { id, requests: [] };
326
- batch.forEach(({ request, requestTime }) => {
327
- const existingRequestEntry = page.requests.find(({ request: storedRequest }) => deepEquals(storedRequest, request));
328
- if (existingRequestEntry === undefined) {
329
- page.requests.push({
330
- request,
331
- requestMetadata: {
332
- requestTime,
333
- },
334
- });
335
- }
336
- else if (requestTime < existingRequestEntry.requestMetadata.requestTime) {
337
- existingRequestEntry.requestMetadata.requestTime = requestTime;
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);
338
636
  }
339
637
  });
340
- setPromises.push(this.storage.set(id, page));
341
- }
342
- this.clearRequestBuffer();
343
- await Promise.all(setPromises);
344
- }
345
- getKeyId(key) {
346
- return stableJSONStringify$1(key);
347
- }
348
- async saveRequest(key, request) {
349
- const identifier = this.getKeyId(key);
350
- const batchForKey = this.requestBuffer.get(identifier) || [];
351
- batchForKey.push({
352
- request,
353
- requestTime: Date.now(),
354
- });
355
- this.requestBuffer.set(identifier, batchForKey);
356
- }
357
- getPage(key) {
358
- const identifier = stableJSONStringify$1(key);
359
- return this.storage.get(identifier);
360
- }
361
- getPageRequests(key) {
362
- const page = this.getPage(key);
363
- if (page === undefined) {
364
- return [];
365
638
  }
366
- return page.requests.map((requestEntry) => requestEntry.request);
367
- }
368
- }
369
-
370
- class RequestStrategy {
371
- transformForSave(request) {
372
- return request;
373
- }
374
- reduce(requests) {
375
- return requests;
639
+ registerPeriodicLogger(NAMESPACE, this.logRefreshStats.bind(this));
376
640
  }
377
- }
378
-
379
- class LuvioAdapterRequestStrategy extends RequestStrategy {
380
641
  /**
381
- * Perform any transformations required to prepare the request for saving.
382
- *
383
- * e.g. If the request is for a record, we move all fields in the fields array
384
- * into the optionalFields array
385
- *
386
- * @param request - The request to transform
387
- * @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.
388
647
  */
389
- transformForSave(request) {
390
- return request;
391
- }
392
- /**
393
- * Filter requests to only those that are for this strategy.
394
- *
395
- * @param unfilteredRequests array of requests to filter
396
- * @returns
397
- */
398
- filterRequests(unfilteredRequests) {
399
- return unfilteredRequests.filter((request) => request.adapterName === this.adapterName);
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);
690
+ }
691
+ }
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);
400
702
  }
401
703
  /**
402
- * Reduce requests by combining those based on a strategies implementations
403
- * of canCombine and combineRequests.
404
- * @param unfilteredRequests array of requests to filter
405
- * @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.
406
711
  */
407
- reduce(unfilteredRequests) {
408
- const requests = this.filterRequests(unfilteredRequests);
409
- const visitedRequests = new Set();
410
- const reducedRequests = [];
411
- for (let i = 0, n = requests.length; i < n; i++) {
412
- const currentRequest = requests[i];
413
- if (!visitedRequests.has(currentRequest)) {
414
- const combinedRequest = { ...currentRequest };
415
- for (let j = i + 1; j < n; j++) {
416
- const hasNotBeenVisited = !visitedRequests.has(requests[j]);
417
- const canCombineConfigs = this.canCombine(combinedRequest.config, requests[j].config);
418
- if (hasNotBeenVisited && canCombineConfigs) {
419
- combinedRequest.config = this.combineRequests(combinedRequest.config, requests[j].config);
420
- visitedRequests.add(requests[j]);
421
- }
422
- }
423
- reducedRequests.push(combinedRequest);
424
- visitedRequests.add(currentRequest);
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();
425
720
  }
426
721
  }
427
- return reducedRequests;
428
722
  }
429
723
  /**
430
- * Check if two requests can be combined.
724
+ * Injected to LDS for Luvio specific instrumentation.
431
725
  *
432
- * By default, requests are not combinable.
433
- * @param reqA config of request A
434
- * @param reqB config of request B
435
- * @returns
726
+ * @param context The transaction context.
436
727
  */
437
- canCombine(_reqA, _reqB) {
438
- return false;
728
+ instrumentLuvio(context) {
729
+ instrumentLuvio(context);
730
+ if (this.isRefreshAdapterEvent(context)) {
731
+ this.aggregateRefreshAdapterEvents(context);
732
+ }
733
+ else if (this.isAdapterUnfulfilledError(context)) {
734
+ this.incrementAdapterRequestErrorCount(context);
735
+ }
736
+ else ;
439
737
  }
440
738
  /**
441
- * Takes two request configs and combines them into a single request config.
442
- *
443
- * @param reqA config of request A
444
- * @param reqB config of request B
445
- * @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.
446
742
  */
447
- combineRequests(reqA, _reqB) {
448
- // By default, this should never be called since requests aren't combinable
449
- if (process.env.NODE_ENV !== 'production') {
450
- throw new Error('Not implemented');
451
- }
452
- return reqA;
743
+ isRefreshAdapterEvent(context) {
744
+ return context[REFRESH_ADAPTER_EVENT] === true;
453
745
  }
454
746
  /**
455
- * Checks adapter config against request context to determine if the request is context dependent.
456
- *
457
- * By default, requests are not context dependent.
458
- * @param request
459
- * @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.
460
750
  */
461
- isContextDependent(_context, _request) {
462
- return false;
751
+ isAdapterUnfulfilledError(context) {
752
+ return context[ADAPTER_UNFULFILLED_ERROR] === true;
463
753
  }
464
754
  /**
465
- * Builds request for saving,
466
- * - transforming the request
467
- * - handling the cases where the request is context dependent (this is homework for the subclass)
468
- * @param _similarContext Context with at least one parameter as a wildcard '*'
469
- * @param context Exact context for a given page
470
- * @param request
471
- * @returns
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
472
760
  */
473
- buildSaveRequestData(_similarContext, context, request) {
474
- return {
475
- request: this.transformForSave(request),
476
- context,
477
- };
478
- }
479
- }
480
-
481
- function normalizeRecordIds$1(recordIds) {
482
- if (!Array.isArray(recordIds)) {
483
- return [recordIds];
484
- }
485
- return recordIds;
486
- }
487
- class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
488
- constructor() {
489
- super(...arguments);
490
- this.adapterName = 'getRecordAvatars';
491
- this.adapterFactory = getRecordAvatarsAdapterFactory;
492
- }
493
- buildConcreteRequest(similarRequest, context) {
494
- return {
495
- ...similarRequest,
496
- config: {
497
- ...similarRequest.config,
498
- recordIds: [context.recordId],
499
- },
500
- };
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
+ }
501
769
  }
502
- buildSaveRequestData(similarContext, context, request) {
503
- if (this.isContextDependent(context, request)) {
504
- return {
505
- request: this.transformForSave({
506
- ...request,
507
- config: {
508
- ...request.config,
509
- recordIds: ['*'],
510
- },
511
- }),
512
- 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,
513
780
  };
514
781
  }
515
- return {
516
- request: this.transformForSave(request),
517
- context,
518
- };
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
+ }
519
788
  }
520
- isContextDependent(context, request) {
521
- return (request.config.recordIds &&
522
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
523
- (request.config.recordIds.length === 1 &&
524
- request.config.recordIds[0] === context.recordId)));
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;
525
819
  }
526
- canCombine(reqA, reqB) {
527
- return reqA.formFactor === reqB.formFactor;
528
- }
529
- combineRequests(reqA, reqB) {
530
- const combined = { ...reqA };
531
- combined.recordIds = Array.from(new Set([...normalizeRecordIds$1(reqA.recordIds), ...normalizeRecordIds$1(reqB.recordIds)]));
532
- return combined;
533
- }
534
- }
535
-
536
- class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
537
- constructor() {
538
- super(...arguments);
539
- this.adapterName = 'getRecord';
540
- this.adapterFactory = getRecordAdapterFactory;
541
- }
542
- buildConcreteRequest(similarRequest, context) {
543
- return {
544
- ...similarRequest,
545
- config: {
546
- ...similarRequest.config,
547
- recordId: context.recordId,
548
- },
549
- };
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;
550
829
  }
551
- transformForSave(request) {
552
- if (request.config.fields === undefined && request.config.optionalFields === undefined) {
553
- return request;
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();
554
838
  }
555
- let fields = coerceFieldIdArray(request.config.fields) || [];
556
- let optionalFields = coerceFieldIdArray(request.config.optionalFields) || [];
557
- return {
558
- ...request,
559
- config: {
560
- ...request.config,
561
- fields: undefined,
562
- optionalFields: [...fields, ...optionalFields],
563
- },
564
- };
565
- }
566
- canCombine(reqA, reqB) {
567
- // must be same record and
568
- return (reqA.recordId === reqB.recordId &&
569
- // both requests are fields requests
570
- (reqA.optionalFields !== undefined || reqB.optionalFields !== undefined) &&
571
- (reqB.fields !== undefined || reqB.optionalFields !== undefined));
572
839
  }
573
- combineRequests(reqA, reqB) {
574
- const fields = new Set();
575
- const optionalFields = new Set();
576
- if (reqA.fields !== undefined) {
577
- reqA.fields.forEach((field) => fields.add(field));
578
- }
579
- if (reqB.fields !== undefined) {
580
- reqB.fields.forEach((field) => fields.add(field));
581
- }
582
- if (reqA.optionalFields !== undefined) {
583
- reqA.optionalFields.forEach((field) => optionalFields.add(field));
584
- }
585
- if (reqB.optionalFields !== undefined) {
586
- reqB.optionalFields.forEach((field) => optionalFields.add(field));
587
- }
588
- return {
589
- recordId: reqA.recordId,
590
- fields: Array.from(fields),
591
- optionalFields: Array.from(optionalFields),
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,
592
850
  };
851
+ this.lastRefreshApiCall = null;
593
852
  }
594
- isContextDependent(context, request) {
595
- return request.config.recordId === context.recordId;
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);
596
869
  }
597
- buildSaveRequestData(similarContext, context, request) {
598
- if (this.isContextDependent(context, request)) {
599
- return {
600
- request: this.transformForSave({
601
- ...request,
602
- config: {
603
- ...request.config,
604
- recordId: '*',
605
- },
606
- }),
607
- context: similarContext,
608
- };
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;
609
883
  }
610
- return {
611
- request: this.transformForSave(request),
612
- context,
613
- };
884
+ adapterRequestErrorCounter.increment(1);
885
+ totalAdapterErrorMetric.increment(1);
614
886
  }
615
887
  }
616
-
617
- class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
618
- constructor() {
619
- super(...arguments);
620
- this.adapterName = 'getRecords';
621
- this.adapterFactory = getRecordsAdapterFactory;
622
- }
623
- buildConcreteRequest(similarRequest, context) {
624
- return {
625
- ...similarRequest,
626
- config: {
627
- ...similarRequest.config,
628
- records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
629
- },
630
- };
631
- }
632
- isContextDependent(context, request) {
633
- const isSingleRecordRequest = request.config.records.length === 1 && request.config.records[0].recordIds.length === 1;
634
- return isSingleRecordRequest && request.config.records[0].recordIds[0] === context.recordId;
888
+ function createMetricsKey(owner, name, unit) {
889
+ let metricName = name;
890
+ if (unit) {
891
+ metricName = metricName + '.' + unit;
635
892
  }
636
- buildSaveRequestData(similarContext, context, request) {
637
- if (this.isContextDependent(context, request)) {
638
- return {
639
- request: this.transformForSave({
640
- ...request,
641
- config: {
642
- ...request.config,
643
- records: [
644
- {
645
- ...request.config.records[0],
646
- recordIds: ['*'],
647
- },
648
- ],
649
- },
650
- }),
651
- context: similarContext,
652
- };
653
- }
654
- return {
655
- request: this.transformForSave(request),
656
- context: context,
657
- };
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;
658
921
  }
922
+ return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
659
923
  }
660
-
661
- function normalizeRecordIds(recordIds) {
662
- if (!ArrayIsArray$1(recordIds)) {
663
- return [recordIds];
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;
664
935
  }
665
- return recordIds;
936
+ timerMetricAddDuration(metric, duration);
666
937
  }
667
- function normalizeApiNames(apiNames) {
668
- if (apiNames === undefined || apiNames === null) {
669
- return [];
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);
670
942
  }
671
- return ArrayIsArray$1(apiNames) ? apiNames : [apiNames];
672
943
  }
673
- class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
674
- constructor() {
675
- super(...arguments);
676
- this.adapterName = 'getRecordActions';
677
- this.adapterFactory = getRecordActionsAdapterFactory;
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;
678
955
  }
679
- buildConcreteRequest(similarRequest, context) {
680
- return {
681
- ...similarRequest,
682
- config: {
683
- ...similarRequest.config,
684
- recordIds: [context.recordId],
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
+ });
685
1021
  },
686
- };
1022
+ getRecordAggregateReject: (cb) => {
1023
+ const recordId = cb();
1024
+ logCRUDLightningInteraction('read', {
1025
+ recordId,
1026
+ state: 'ERROR',
1027
+ });
1028
+ },
1029
+ });
687
1030
  }
688
- buildSaveRequestData(similarContext, context, request) {
689
- if (this.isContextDependent(context, request)) {
690
- return {
691
- request: this.transformForSave({
692
- ...request,
693
- config: {
694
- ...request.config,
695
- recordIds: ['*'],
696
- },
697
- }),
698
- context: similarContext,
699
- };
700
- }
701
- return {
702
- request: this.transformForSave(request),
703
- context,
704
- };
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();
705
1062
  }
706
- canCombine(reqA, reqB) {
707
- return (reqA.retrievalMode === reqB.retrievalMode &&
708
- reqA.formFactor === reqB.formFactor &&
709
- (reqA.actionTypes || []).toString() === (reqB.actionTypes || []).toString() &&
710
- (reqA.sections || []).toString() === (reqB.sections || []).toString());
1063
+ set context(value) {
1064
+ this._context = value;
1065
+ this.page = this.getPage();
711
1066
  }
712
- combineRequests(reqA, reqB) {
713
- const combined = { ...reqA };
714
- // let's merge the recordIds
715
- combined.recordIds = Array.from(new Set([...normalizeRecordIds(reqA.recordIds), ...normalizeRecordIds(reqB.recordIds)]));
716
- if (combined.retrievalMode === 'ALL') {
717
- const combinedSet = new Set([
718
- ...normalizeApiNames(combined.apiNames),
719
- ...normalizeApiNames(reqB.apiNames),
720
- ]);
721
- combined.apiNames = Array.from(combinedSet);
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();
722
1081
  }
723
- return combined;
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
+ });
724
1088
  }
725
- isContextDependent(context, request) {
726
- return (request.config.recordIds &&
727
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
728
- (request.config.recordIds.length === 1 &&
729
- request.config.recordIds[0] === context.recordId)));
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();
730
1103
  }
731
- }
732
-
733
- class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
734
- constructor() {
735
- super(...arguments);
736
- this.adapterName = 'getObjectInfo';
737
- this.adapterFactory = getObjectInfoAdapterFactory;
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 };
738
1110
  }
739
- buildConcreteRequest(similarRequest, context) {
740
- return {
741
- ...similarRequest,
742
- config: {
743
- ...similarRequest.config,
744
- objectApiName: context.objectApiName,
745
- },
746
- };
1111
+ hasPredictions() {
1112
+ const summary = this.getPredictionSummary();
1113
+ return summary.exact > 0 || summary.similar > 0;
747
1114
  }
748
- buildSaveRequestData(similarContext, context, request) {
749
- if (this.isContextDependent(context, request)) {
750
- return {
751
- request: this.transformForSave(request),
752
- context: similarContext,
753
- };
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
+ }
754
1122
  }
755
- return {
756
- request: this.transformForSave(request),
757
- context,
758
- };
759
- }
760
- isContextDependent(context, request) {
761
- return (request.config.objectApiName !== undefined &&
762
- context.objectApiName === request.config.objectApiName);
1123
+ return resolvedSimilarPageRequests;
763
1124
  }
764
1125
  }
765
1126
 
766
- class GetObjectInfosRequestStrategy extends LuvioAdapterRequestStrategy {
767
- constructor() {
768
- super(...arguments);
769
- this.adapterName = 'getObjectInfos';
770
- this.adapterFactory = getObjectInfosAdapterFactory;
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();
771
1135
  }
772
- buildConcreteRequest(similarRequest) {
773
- return similarRequest;
1136
+ getPage() {
1137
+ if (RecordHomePage.handlesContext(this.context)) {
1138
+ return new RecordHomePage(this.context, this.requestStrategies);
1139
+ }
1140
+ return new LexDefaultPage(this.context);
774
1141
  }
775
1142
  }
776
1143
 
777
- const { keys: ObjectKeys$1 } = Object;
778
- const { isArray: ArrayIsArray, from: ArrayFrom } = Array;
779
- function isReduceAbleRelatedListConfig(config) {
780
- return config.relatedListsActionParameters.every((rlReq) => {
781
- return rlReq.relatedListId !== undefined && ObjectKeys$1(rlReq).length === 1;
782
- });
783
- }
784
- class GetRelatedListsActionsRequestStrategy extends LuvioAdapterRequestStrategy {
785
- constructor() {
786
- super(...arguments);
787
- this.adapterName = 'getRelatedListsActions';
788
- this.adapterFactory = getRelatedListsActionsAdapterFactory;
789
- }
790
- buildConcreteRequest(similarRequest, context) {
791
- return {
792
- ...similarRequest,
793
- config: {
794
- ...similarRequest.config,
795
- recordIds: [context.recordId],
796
- },
797
- };
798
- }
799
- buildSaveRequestData(similarContext, context, request) {
800
- if (this.isContextDependent(context, request)) {
801
- return {
802
- request: this.transformForSave({
803
- ...request,
804
- config: {
805
- ...request.config,
806
- recordIds: ['*'],
807
- },
808
- }),
809
- context: similarContext,
810
- };
811
- }
812
- return {
813
- request: this.transformForSave(request),
814
- context,
815
- };
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();
816
1163
  }
817
- isContextDependent(context, request) {
818
- const isForContext = request.config.recordIds &&
819
- (context.recordId === request.config.recordIds || // some may set this as string instead of array
820
- (request.config.recordIds.length === 1 &&
821
- request.config.recordIds[0] === context.recordId));
822
- return isForContext && isReduceAbleRelatedListConfig(request.config);
1164
+ if (node === undefined) {
1165
+ return;
823
1166
  }
824
- /**
825
- * Can only reduce two requests when they have the same recordId, and
826
- * the individual relatedListAction config only have relatedListId.
827
- *
828
- * @param reqA
829
- * @param reqB
830
- * @returns boolean
831
- */
832
- canCombine(reqA, reqB) {
833
- const [recordIdA, recordIdB] = [reqA.recordIds, reqB.recordIds].map((recordIds) => {
834
- return ArrayIsArray(recordIds)
835
- ? recordIds.length === 1
836
- ? recordIds[0]
837
- : null
838
- : recordIds;
839
- });
840
- return (recordIdA === recordIdB &&
841
- recordIdA !== null &&
842
- isReduceAbleRelatedListConfig(reqA) &&
843
- isReduceAbleRelatedListConfig(reqB));
1167
+ if (typeof node === 'number') {
1168
+ return isFinite(node) ? '' + node : 'null';
844
1169
  }
845
- combineRequests(reqA, reqB) {
846
- const relatedListsIncluded = new Set();
847
- [reqA, reqB].forEach(({ relatedListsActionParameters }) => {
848
- relatedListsActionParameters.forEach(({ relatedListId }) => relatedListsIncluded.add(relatedListId));
849
- });
850
- return {
851
- recordIds: reqA.recordIds,
852
- relatedListsActionParameters: ArrayFrom(relatedListsIncluded).map((relatedListId) => ({
853
- relatedListId,
854
- })),
855
- };
1170
+ if (typeof node !== 'object') {
1171
+ return JSONStringify(node);
856
1172
  }
857
- }
858
-
859
- class GetRelatedListInfoBatchRequestStrategy extends LuvioAdapterRequestStrategy {
860
- constructor() {
861
- super(...arguments);
862
- this.adapterName = 'getRelatedListInfoBatch';
863
- this.adapterFactory = getRelatedListInfoBatchAdapterFactory;
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';
1182
+ }
1183
+ return out + ']';
864
1184
  }
865
- buildConcreteRequest(similarRequest, _context) {
866
- return similarRequest;
1185
+ if (node === null) {
1186
+ return 'null';
867
1187
  }
868
- canCombine(reqA, reqB) {
869
- return reqA.parentObjectApiName === reqB.parentObjectApiName;
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;
1195
+ }
1196
+ if (out) {
1197
+ out += ',';
1198
+ }
1199
+ out += JSONStringify(key) + ':' + value;
870
1200
  }
871
- combineRequests(reqA, reqB) {
872
- const combined = { ...reqA };
873
- combined.relatedListNames = Array.from(new Set([...reqA.relatedListNames, ...reqB.relatedListNames]));
874
- return combined;
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;
875
1230
  }
1231
+ return true;
876
1232
  }
877
1233
 
878
- const GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME = 'getRelatedListRecordsBatch';
879
- class GetRelatedListRecordsBatchRequestStrategy extends LuvioAdapterRequestStrategy {
880
- constructor() {
881
- super(...arguments);
882
- this.adapterName = GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME;
883
- this.adapterFactory = getRelatedListRecordsBatchAdapterFactory;
1234
+ class PrefetchRepository {
1235
+ constructor(storage) {
1236
+ this.storage = storage;
1237
+ this.requestBuffer = new Map();
884
1238
  }
885
- buildConcreteRequest(similarRequest, context) {
886
- return {
887
- ...similarRequest,
888
- config: {
889
- ...similarRequest.config,
890
- parentRecordId: context.recordId,
891
- },
892
- };
1239
+ clearRequestBuffer() {
1240
+ this.requestBuffer.clear();
893
1241
  }
894
- buildSaveRequestData(similarContext, context, request) {
895
- if (this.isContextDependent(context, request)) {
896
- return {
897
- request: this.transformForSave({
898
- ...request,
899
- config: {
900
- ...request.config,
901
- parentRecordId: '*',
902
- },
903
- }),
904
- context: similarContext,
905
- };
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));
906
1261
  }
907
- return {
908
- request: this.transformForSave(request),
909
- context,
910
- };
911
- }
912
- isContextDependent(context, request) {
913
- return context.recordId === request.config.parentRecordId;
1262
+ this.clearRequestBuffer();
1263
+ await Promise.all(setPromises);
914
1264
  }
915
- /**
916
- * Can combine two seperate batch requests if the parentRecordId is the same.
917
- * @param reqA The first GetRelatedListRecordsBatchConfig.
918
- * @param reqB The first GetRelatedListRecordsBatchConfig.
919
- * @returns true if the requests can be combined, otherwise false.
920
- */
921
- canCombine(reqA, reqB) {
922
- return reqA.parentRecordId === reqB.parentRecordId;
1265
+ getKeyId(key) {
1266
+ return stableJSONStringify(key);
923
1267
  }
924
- /**
925
- * Merge the relatedListParameters together between two combinable batch requests.
926
- * @param reqA The first GetRelatedListRecordsBatchConfig.
927
- * @param reqB The first GetRelatedListRecordsBatchConfig.
928
- * @returns The combined request.
929
- */
930
- combineRequests(reqA, reqB) {
931
- const relatedListParametersMap = new Set(reqA.relatedListParameters.map((relatedListParameter) => {
932
- return stableJSONStringify$1(relatedListParameter);
933
- }));
934
- const reqBRelatedListParametersToAdd = reqB.relatedListParameters.filter((relatedListParameter) => {
935
- return !relatedListParametersMap.has(stableJSONStringify$1(relatedListParameter));
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(),
936
1274
  });
937
- reqA.relatedListParameters = reqA.relatedListParameters.concat(reqBRelatedListParametersToAdd);
938
- return reqA;
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 [];
1285
+ }
1286
+ return page.requests.map((requestEntry) => requestEntry.request);
939
1287
  }
940
1288
  }
941
1289
 
942
- class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
1290
+ class RequestStrategy {
1291
+ transformForSave(request) {
1292
+ return request;
1293
+ }
1294
+ reduce(requests) {
1295
+ return requests;
1296
+ }
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;
1359
+ }
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');
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) {
1394
+ return {
1395
+ request: this.transformForSave(request),
1396
+ context,
1397
+ };
1398
+ }
1399
+ }
1400
+
1401
+ function normalizeRecordIds$1(recordIds) {
1402
+ if (!Array.isArray(recordIds)) {
1403
+ return [recordIds];
1404
+ }
1405
+ return recordIds;
1406
+ }
1407
+ class GetRecordAvatarsRequestStrategy extends LuvioAdapterRequestStrategy {
943
1408
  constructor() {
944
1409
  super(...arguments);
945
- this.adapterName = 'getRelatedListRecords';
946
- this.adapterFactory = getRelatedListRecordsAdapterFactory;
1410
+ this.adapterName = 'getRecordAvatars';
1411
+ this.adapterFactory = getRecordAvatarsAdapterFactory;
947
1412
  }
948
1413
  buildConcreteRequest(similarRequest, context) {
949
1414
  return {
950
1415
  ...similarRequest,
951
1416
  config: {
952
1417
  ...similarRequest.config,
953
- parentRecordId: context.recordId,
1418
+ recordIds: [context.recordId],
954
1419
  },
955
1420
  };
956
1421
  }
957
- /**
958
- *
959
- * This method returns GetRelatedListRecordsRequest[] that won't be part of a batch request.
960
- *
961
- * ADG currently handles the batching of GetRelatedListRecords -> GetRelatedListRecordsBatch
962
- * https://gitcore.soma.salesforce.com/core-2206/core-public/blob/p4/main/core/ui-laf-components/modules/laf/batchingPortable/reducers/RelatedListRecordsBatchReducer.js
963
- *
964
- * For performance reasons (fear to overfetch), we only check that the Single relatedListId is not present in any of the Batch requests,
965
- * but we don't check for any other parameters.
966
- *
967
- * @param unfilteredRequests All of the request available for predictions.
968
- * @returns GetRelatedListRecordsRequest[] That should be a prediction.
969
- */
970
- reduce(unfilteredRequests) {
971
- // Batch requests by [parentRecordId]->[RelatedListIds]
972
- const batchRequests = unfilteredRequests.filter((request) => request.adapterName === GET_RELATED_LIST_RECORDS_BATCH_ADAPTER_NAME).reduce((acc, request) => {
973
- // required properties, enforced by adapter typecheck
974
- const { parentRecordId, relatedListParameters } = request.config;
975
- const existingRlSet = acc.get(parentRecordId) || new Set();
976
- // relatedListId enforced by adapter typecheck
977
- relatedListParameters.forEach((rlParam) => existingRlSet.add(rlParam.relatedListId));
978
- acc.set(parentRecordId, existingRlSet);
979
- return acc;
980
- }, new Map());
981
- const singleRequests = unfilteredRequests.filter((request) => request.adapterName === this.adapterName);
982
- return singleRequests.filter((request) => {
983
- // required props enforced by adapter typecheck
984
- const { parentRecordId, relatedListId } = request.config;
985
- const batchForParentRecordId = batchRequests.get(parentRecordId);
986
- return !(batchForParentRecordId && batchForParentRecordId.has(relatedListId));
987
- });
988
- }
989
1422
  buildSaveRequestData(similarContext, context, request) {
990
1423
  if (this.isContextDependent(context, request)) {
991
1424
  return {
@@ -993,7 +1426,7 @@ class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
993
1426
  ...request,
994
1427
  config: {
995
1428
  ...request.config,
996
- parentRecordId: '*',
1429
+ recordIds: ['*'],
997
1430
  },
998
1431
  }),
999
1432
  context: similarContext,
@@ -1005,684 +1438,597 @@ class GetRelatedListRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
1005
1438
  };
1006
1439
  }
1007
1440
  isContextDependent(context, request) {
1008
- return context.recordId === request.config.parentRecordId;
1009
- }
1010
- }
1011
-
1012
- class LexRequestRunner {
1013
- constructor(luvio) {
1014
- this.luvio = luvio;
1015
- this.requestStrategies = {
1016
- getRecord: new GetRecordRequestStrategy(),
1017
- getRecords: new GetRecordsRequestStrategy(),
1018
- getRecordActions: new GetRecordActionsRequestStrategy(),
1019
- getRecordAvatars: new GetRecordAvatarsRequestStrategy(),
1020
- getObjectInfo: new GetObjectInfoRequestStrategy(),
1021
- getObjectInfos: new GetObjectInfosRequestStrategy(),
1022
- getRelatedListsActions: new GetRelatedListsActionsRequestStrategy(),
1023
- getRelatedListInfoBatch: new GetRelatedListInfoBatchRequestStrategy(),
1024
- getRelatedListRecords: new GetRelatedListRecordsRequestStrategy(),
1025
- getRelatedListRecordsBatch: new GetRelatedListRecordsBatchRequestStrategy(),
1026
- };
1441
+ return (request.config.recordIds &&
1442
+ (context.recordId === request.config.recordIds || // some may set this as string instead of array
1443
+ (request.config.recordIds.length === 1 &&
1444
+ request.config.recordIds[0] === context.recordId)));
1027
1445
  }
1028
- reduceRequests(requests) {
1029
- return Object.values(this.requestStrategies)
1030
- .map((strategy) => strategy.reduce(requests))
1031
- .flat();
1446
+ canCombine(reqA, reqB) {
1447
+ return reqA.formFactor === reqB.formFactor;
1032
1448
  }
1033
- runRequest(request) {
1034
- if (request.adapterName in this.requestStrategies) {
1035
- const adapterFactory = this.requestStrategies[request.adapterName].adapterFactory;
1036
- return Promise.resolve(adapterFactory(this.luvio)(request.config)).then();
1037
- }
1038
- return Promise.resolve(undefined);
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;
1039
1453
  }
1040
1454
  }
1041
1455
 
1042
- class InMemoryPrefetchStorage {
1456
+ class GetRecordRequestStrategy extends LuvioAdapterRequestStrategy {
1043
1457
  constructor() {
1044
- this.data = {};
1045
- }
1046
- set(key, value) {
1047
- this.data[key] = value;
1048
- return Promise.resolve();
1458
+ super(...arguments);
1459
+ this.adapterName = 'getRecord';
1460
+ this.adapterFactory = getRecordAdapterFactory;
1049
1461
  }
1050
- get(key) {
1051
- return this.data[key];
1462
+ buildConcreteRequest(similarRequest, context) {
1463
+ return {
1464
+ ...similarRequest,
1465
+ config: {
1466
+ ...similarRequest.config,
1467
+ recordId: context.recordId,
1468
+ },
1469
+ };
1052
1470
  }
1053
- }
1054
-
1055
- const { keys: ObjectKeys } = Object;
1056
- const DEFAULT_STORAGE_OPTIONS = {
1057
- name: 'ldsPredictiveLoading',
1058
- persistent: true,
1059
- secure: true,
1060
- maxSize: 7 * 1024 * 1024,
1061
- expiration: 12 * 60 * 60,
1062
- clearOnInit: false,
1063
- debugLogging: false,
1064
- version: 3,
1065
- };
1066
- function buildAuraPrefetchStorage(options = {}) {
1067
- const auraStorage = createStorage({
1068
- ...DEFAULT_STORAGE_OPTIONS,
1069
- ...options,
1070
- });
1071
- const inMemoryStorage = new InMemoryPrefetchStorage();
1072
- if (auraStorage === null) {
1073
- return inMemoryStorage;
1471
+ transformForSave(request) {
1472
+ if (request.config.fields === undefined && request.config.optionalFields === undefined) {
1473
+ return request;
1474
+ }
1475
+ let fields = coerceFieldIdArray(request.config.fields) || [];
1476
+ let optionalFields = coerceFieldIdArray(request.config.optionalFields) || [];
1477
+ return {
1478
+ ...request,
1479
+ config: {
1480
+ ...request.config,
1481
+ fields: undefined,
1482
+ optionalFields: [...fields, ...optionalFields],
1483
+ },
1484
+ };
1074
1485
  }
1075
- return new AuraPrefetchStorage(auraStorage, inMemoryStorage);
1076
- }
1077
- class AuraPrefetchStorage {
1078
- constructor(auraStorage, inMemoryStorage) {
1079
- this.auraStorage = auraStorage;
1080
- this.inMemoryStorage = inMemoryStorage;
1081
- /**
1082
- * Because of aura is an event loop hog and we therefore need to minimize asynchronicity in LEX,
1083
- * we need need to preload predictions and treat read operations sync. Not making it sync, will cause
1084
- * some request to be sent the network when they could be dedupe against those from the predictions.
1085
- *
1086
- * Drawbacks of this approach:
1087
- * 1. Loading all of aura storage into memory and then updating it based on changes to that in memory
1088
- * representation means that updates to predictions in aura storage across different tabs will result
1089
- * in overwrites, not graceful merges of predictions.
1090
- * 2. If whoever is consuming this tries to get and run predictions before this is done loading,
1091
- * then they will (potentially incorrectly) think that we don't have any predictions.
1092
- */
1093
- auraStorage.getAll().then((results) => {
1094
- ObjectKeys(results).forEach((key) => this.inMemoryStorage.set(key, results[key]));
1095
- });
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));
1096
1492
  }
1097
- set(key, value) {
1098
- const inMemoryResult = this.inMemoryStorage.set(key, value);
1099
- this.auraStorage.set(key, value).catch((error) => {
1100
- if (process.env.NODE_ENV !== 'production') {
1101
- // eslint-disable-next-line no-console
1102
- console.error('Error save LDS prediction: ', error);
1103
- }
1104
- });
1105
- return inMemoryResult;
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
+ }
1508
+ return {
1509
+ recordId: reqA.recordId,
1510
+ fields: Array.from(fields),
1511
+ optionalFields: Array.from(optionalFields),
1512
+ };
1106
1513
  }
1107
- get(key) {
1108
- // we never read from the AuraStorage, except in construction.
1109
- return this.inMemoryStorage.get(key);
1514
+ isContextDependent(context, request) {
1515
+ return request.config.recordId === context.recordId;
1516
+ }
1517
+ buildSaveRequestData(similarContext, context, request) {
1518
+ if (this.isContextDependent(context, request)) {
1519
+ return {
1520
+ request: this.transformForSave({
1521
+ ...request,
1522
+ config: {
1523
+ ...request.config,
1524
+ recordId: '*',
1525
+ },
1526
+ }),
1527
+ context: similarContext,
1528
+ };
1529
+ }
1530
+ return {
1531
+ request: this.transformForSave(request),
1532
+ context,
1533
+ };
1110
1534
  }
1111
1535
  }
1112
1536
 
1113
- /**
1114
- * Observability / Critical Availability Program (230+)
1115
- *
1116
- * This file is intended to be used as a consolidated place for all definitions, functions,
1117
- * and helpers related to "M1"[1].
1118
- *
1119
- * Below are the R.E.A.D.S. metrics for the Lightning Data Service, defined here[2].
1120
- *
1121
- * [1] Search "[M1] Lightning Data Service Design Spike" in Quip
1122
- * [2] Search "Lightning Data Service R.E.A.D.S. Metrics" in Quip
1123
- */
1124
- const OBSERVABILITY_NAMESPACE = 'LIGHTNING.lds.service';
1125
- const ADAPTER_INVOCATION_COUNT_METRIC_NAME = 'request';
1126
- const ADAPTER_ERROR_COUNT_METRIC_NAME = 'error';
1127
- const NETWORK_ADAPTER_RESPONSE_METRIC_NAME = 'network-response';
1128
- /**
1129
- * W-8379680
1130
- * Counter for number of getApex requests.
1131
- */
1132
- const GET_APEX_REQUEST_COUNT = {
1133
- get() {
1537
+ class GetRecordsRequestStrategy extends LuvioAdapterRequestStrategy {
1538
+ constructor() {
1539
+ super(...arguments);
1540
+ this.adapterName = 'getRecords';
1541
+ this.adapterFactory = getRecordsAdapterFactory;
1542
+ }
1543
+ buildConcreteRequest(similarRequest, context) {
1134
1544
  return {
1135
- owner: OBSERVABILITY_NAMESPACE,
1136
- name: ADAPTER_INVOCATION_COUNT_METRIC_NAME + '.' + NORMALIZED_APEX_ADAPTER_NAME,
1545
+ ...similarRequest,
1546
+ config: {
1547
+ ...similarRequest.config,
1548
+ records: [{ ...similarRequest.config.records[0], recordIds: [context.recordId] }],
1549
+ },
1137
1550
  };
1138
- },
1139
- };
1140
- /**
1141
- * W-8828410
1142
- * Counter for the number of UnfulfilledSnapshotErrors the luvio engine has.
1143
- */
1144
- const TOTAL_ADAPTER_ERROR_COUNT = {
1145
- get() {
1146
- return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_ERROR_COUNT_METRIC_NAME };
1147
- },
1148
- };
1149
- /**
1150
- * W-8828410
1151
- * Counter for the number of invocations made into LDS by a wire adapter.
1152
- */
1153
- const TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT = {
1154
- get() {
1155
- return { owner: OBSERVABILITY_NAMESPACE, name: ADAPTER_INVOCATION_COUNT_METRIC_NAME };
1156
- },
1157
- };
1158
-
1159
- const { create, keys } = Object;
1160
- const { isArray } = Array;
1161
- const { stringify } = JSON;
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
+ }
1556
+ buildSaveRequestData(similarContext, context, request) {
1557
+ if (this.isContextDependent(context, request)) {
1558
+ return {
1559
+ request: this.transformForSave({
1560
+ ...request,
1561
+ config: {
1562
+ ...request.config,
1563
+ records: [
1564
+ {
1565
+ ...request.config.records[0],
1566
+ recordIds: ['*'],
1567
+ },
1568
+ ],
1569
+ },
1570
+ }),
1571
+ context: similarContext,
1572
+ };
1573
+ }
1574
+ return {
1575
+ request: this.transformForSave(request),
1576
+ context: context,
1577
+ };
1578
+ }
1579
+ }
1162
1580
 
1163
- /**
1164
- * A deterministic JSON stringify implementation. Heavily adapted from https://github.com/epoberezkin/fast-json-stable-stringify.
1165
- * This is needed because insertion order for JSON.stringify(object) affects output:
1166
- * JSON.stringify({a: 1, b: 2})
1167
- * "{"a":1,"b":2}"
1168
- * JSON.stringify({b: 2, a: 1})
1169
- * "{"b":2,"a":1}"
1170
- * Modified from the apex implementation to sort arrays non-destructively.
1171
- * @param data Data to be JSON-stringified.
1172
- * @returns JSON.stringified value with consistent ordering of keys.
1173
- */
1174
- function stableJSONStringify(node) {
1175
- // This is for Date values.
1176
- if (node && node.toJSON && typeof node.toJSON === 'function') {
1177
- // eslint-disable-next-line no-param-reassign
1178
- node = node.toJSON();
1581
+ function normalizeRecordIds(recordIds) {
1582
+ if (!ArrayIsArray$1(recordIds)) {
1583
+ return [recordIds];
1179
1584
  }
1180
- if (node === undefined) {
1181
- return;
1585
+ return recordIds;
1586
+ }
1587
+ function normalizeApiNames(apiNames) {
1588
+ if (apiNames === undefined || apiNames === null) {
1589
+ return [];
1182
1590
  }
1183
- if (typeof node === 'number') {
1184
- return isFinite(node) ? '' + node : 'null';
1591
+ return ArrayIsArray$1(apiNames) ? apiNames : [apiNames];
1592
+ }
1593
+ class GetRecordActionsRequestStrategy extends LuvioAdapterRequestStrategy {
1594
+ constructor() {
1595
+ super(...arguments);
1596
+ this.adapterName = 'getRecordActions';
1597
+ this.adapterFactory = getRecordActionsAdapterFactory;
1185
1598
  }
1186
- if (typeof node !== 'object') {
1187
- return stringify(node);
1599
+ buildConcreteRequest(similarRequest, context) {
1600
+ return {
1601
+ ...similarRequest,
1602
+ config: {
1603
+ ...similarRequest.config,
1604
+ recordIds: [context.recordId],
1605
+ },
1606
+ };
1188
1607
  }
1189
- let i;
1190
- let out;
1191
- if (isArray(node)) {
1192
- // copy any array before sorting so we don't mutate the object.
1193
- // eslint-disable-next-line no-param-reassign
1194
- node = node.slice(0).sort();
1195
- out = '[';
1196
- for (i = 0; i < node.length; i++) {
1197
- if (i) {
1198
- out += ',';
1199
- }
1200
- out += stableJSONStringify(node[i]) || 'null';
1608
+ buildSaveRequestData(similarContext, context, request) {
1609
+ if (this.isContextDependent(context, request)) {
1610
+ return {
1611
+ request: this.transformForSave({
1612
+ ...request,
1613
+ config: {
1614
+ ...request.config,
1615
+ recordIds: ['*'],
1616
+ },
1617
+ }),
1618
+ context: similarContext,
1619
+ };
1201
1620
  }
1202
- return out + ']';
1621
+ return {
1622
+ request: this.transformForSave(request),
1623
+ context,
1624
+ };
1203
1625
  }
1204
- if (node === null) {
1205
- return 'null';
1626
+ canCombine(reqA, reqB) {
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());
1206
1631
  }
1207
- const keys$1 = keys(node).sort();
1208
- out = '';
1209
- for (i = 0; i < keys$1.length; i++) {
1210
- const key = keys$1[i];
1211
- const value = stableJSONStringify(node[key]);
1212
- if (!value) {
1213
- continue;
1632
+ combineRequests(reqA, reqB) {
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);
1214
1642
  }
1215
- if (out) {
1216
- out += ',';
1643
+ return combined;
1644
+ }
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)));
1650
+ }
1651
+ }
1652
+
1653
+ class GetObjectInfoRequestStrategy extends LuvioAdapterRequestStrategy {
1654
+ constructor() {
1655
+ super(...arguments);
1656
+ this.adapterName = 'getObjectInfo';
1657
+ this.adapterFactory = getObjectInfoAdapterFactory;
1658
+ }
1659
+ buildConcreteRequest(similarRequest, context) {
1660
+ return {
1661
+ ...similarRequest,
1662
+ config: {
1663
+ ...similarRequest.config,
1664
+ objectApiName: context.objectApiName,
1665
+ },
1666
+ };
1667
+ }
1668
+ buildSaveRequestData(similarContext, context, request) {
1669
+ if (this.isContextDependent(context, request)) {
1670
+ return {
1671
+ request: this.transformForSave(request),
1672
+ context: similarContext,
1673
+ };
1217
1674
  }
1218
- 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);
1219
1683
  }
1220
- return '{' + out + '}';
1221
1684
  }
1222
- function isPromise(value) {
1223
- // check for Thenable due to test frameworks using custom Promise impls
1224
- 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
+ }
1225
1695
  }
1226
1696
 
1227
- const APEX_ADAPTER_NAME = 'getApex';
1228
- const NORMALIZED_APEX_ADAPTER_NAME = `Apex.${APEX_ADAPTER_NAME}`;
1229
- const REFRESH_APEX_KEY = 'refreshApex';
1230
- const REFRESH_UIAPI_KEY = 'refreshUiApi';
1231
- const SUPPORTED_KEY = 'refreshSupported';
1232
- const UNSUPPORTED_KEY = 'refreshUnsupported';
1233
- const REFRESH_EVENTSOURCE = 'lds-refresh-summary';
1234
- const REFRESH_EVENTTYPE = 'system';
1235
- const REFRESH_PAYLOAD_TARGET = 'adapters';
1236
- const REFRESH_PAYLOAD_SCOPE = 'lds';
1237
- const INCOMING_WEAKETAG_0_KEY = 'incoming-weaketag-0';
1238
- const EXISTING_WEAKETAG_0_KEY = 'existing-weaketag-0';
1239
- const RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME = 'record-api-name-change-count';
1240
- const NAMESPACE = 'lds';
1241
- const NETWORK_TRANSACTION_NAME = 'lds-network';
1242
- const CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX = 'out-of-ttl-miss';
1243
- // Aggregate Cache Stats and Metrics for all getApex invocations
1244
- const getApexCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME);
1245
- const getApexTtlCacheStats = registerLdsCacheStats(NORMALIZED_APEX_ADAPTER_NAME + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
1246
- // Observability (READS)
1247
- const getApexRequestCountMetric = counter(GET_APEX_REQUEST_COUNT);
1248
- const totalAdapterRequestSuccessMetric = counter(TOTAL_ADAPTER_REQUEST_SUCCESS_COUNT);
1249
- const totalAdapterErrorMetric = counter(TOTAL_ADAPTER_ERROR_COUNT);
1250
- 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 {
1251
1705
  constructor() {
1252
- this.adapterUnfulfilledErrorCounters = {};
1253
- this.recordApiNameChangeCounters = {};
1254
- this.refreshAdapterEvents = {};
1255
- this.refreshApiCallEventStats = {
1256
- [REFRESH_APEX_KEY]: 0,
1257
- [REFRESH_UIAPI_KEY]: 0,
1258
- [SUPPORTED_KEY]: 0,
1259
- [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
+ },
1260
1717
  };
1261
- this.lastRefreshApiCall = null;
1262
- this.weakEtagZeroEvents = {};
1263
- this.adapterCacheMisses = new LRUCache(250);
1264
- if (typeof window !== 'undefined' && window.addEventListener) {
1265
- window.addEventListener('beforeunload', () => {
1266
- if (keys(this.weakEtagZeroEvents).length > 0) {
1267
- perfStart(NETWORK_TRANSACTION_NAME);
1268
- perfEnd(NETWORK_TRANSACTION_NAME, this.weakEtagZeroEvents);
1269
- }
1270
- });
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
+ };
1271
1731
  }
1272
- 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);
1273
1743
  }
1274
1744
  /**
1275
- * Instruments an existing adapter to log argus metrics and cache stats.
1276
- * @param adapter The adapter function.
1277
- * @param metadata The adapter metadata.
1278
- * @param wireConfigKeyFn Optional function to transform wire configs to a unique key.
1279
- * @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
1280
1751
  */
1281
- instrumentAdapter(adapter, metadata) {
1282
- // We are consolidating all apex adapter instrumentation calls under a single key
1283
- const { apiFamily, name, ttl } = metadata;
1284
- const adapterName = normalizeAdapterName(name, apiFamily);
1285
- const isGetApexAdapter = isApexAdapter(name);
1286
- const stats = isGetApexAdapter ? getApexCacheStats : registerLdsCacheStats(adapterName);
1287
- const ttlMissStats = isGetApexAdapter
1288
- ? getApexTtlCacheStats
1289
- : registerLdsCacheStats(adapterName + ':' + CACHE_STATS_OUT_OF_TTL_MISS_POSTFIX);
1290
- /**
1291
- * W-8076905
1292
- * Dynamically generated metric. Simple counter for all requests made by this adapter.
1293
- */
1294
- const wireAdapterRequestMetric = isGetApexAdapter
1295
- ? getApexRequestCountMetric
1296
- : counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_INVOCATION_COUNT_METRIC_NAME, adapterName));
1297
- const instrumentedAdapter = (config, requestContext) => {
1298
- // increment overall and adapter request metrics
1299
- wireAdapterRequestMetric.increment(1);
1300
- totalAdapterRequestSuccessMetric.increment(1);
1301
- // execute adapter logic
1302
- const result = adapter(config, requestContext);
1303
- // In the case where the adapter returns a non-Pending Snapshot it is constructed out of the store
1304
- // (cache hit) whereas a Promise<Snapshot> or Pending Snapshot indicates a network request (cache miss).
1305
- //
1306
- // Note: we can't do a plain instanceof check for a promise here since the Promise may
1307
- // originate from another javascript realm (for example: in jest test). Instead we use a
1308
- // duck-typing approach by checking if the result has a then property.
1309
- //
1310
- // For adapters without persistent store:
1311
- // - total cache hit ratio:
1312
- // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
1313
- // For adapters with persistent store:
1314
- // - in-memory cache hit ratio:
1315
- // [in-memory cache hit count] / ([in-memory cache hit count] + [in-memory cache miss count])
1316
- // - total cache hit ratio:
1317
- // ([in-memory cache hit count] + [store cache hit count]) / ([in-memory cache hit count] + [in-memory cache miss count])
1318
- // if result === null then config is insufficient/invalid so do not log
1319
- if (isPromise(result)) {
1320
- stats.logMisses();
1321
- if (ttl !== undefined) {
1322
- this.logAdapterCacheMissOutOfTtlDuration(adapterName, config, ttlMissStats, Date.now(), ttl);
1323
- }
1324
- }
1325
- else if (result !== null) {
1326
- stats.logHits();
1327
- }
1328
- return result;
1329
- };
1330
- // Set the name property on the function for debugging purposes.
1331
- Object.defineProperty(instrumentedAdapter, 'name', {
1332
- 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;
1333
1759
  });
1334
- return instrumentAdapter(instrumentedAdapter, metadata);
1760
+ return (recordIdA === recordIdB &&
1761
+ recordIdA !== null &&
1762
+ isReduceAbleRelatedListConfig(reqA) &&
1763
+ isReduceAbleRelatedListConfig(reqB));
1335
1764
  }
1336
- /**
1337
- * 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.
1338
- * Backed by an LRU Cache implementation to prevent too many record entries from being stored in-memory.
1339
- * @param name The wire adapter name.
1340
- * @param config The config passed into wire adapter.
1341
- * @param ttlMissStats CacheStatsLogger to log misses out of TTL.
1342
- * @param currentCacheMissTimestamp Timestamp for when the request was made.
1343
- * @param ttl TTL for the wire adapter.
1344
- */
1345
- logAdapterCacheMissOutOfTtlDuration(name, config, ttlMissStats, currentCacheMissTimestamp, ttl) {
1346
- const configKey = `${name}:${stableJSONStringify(config)}`;
1347
- const existingCacheMissTimestamp = this.adapterCacheMisses.get(configKey);
1348
- this.adapterCacheMisses.set(configKey, currentCacheMissTimestamp);
1349
- if (existingCacheMissTimestamp !== undefined) {
1350
- const duration = currentCacheMissTimestamp - existingCacheMissTimestamp;
1351
- if (duration > ttl) {
1352
- ttlMissStats.logMisses();
1353
- }
1354
- }
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;
1355
1784
  }
1356
- /**
1357
- * Injected to LDS for Luvio specific instrumentation.
1358
- *
1359
- * @param context The transaction context.
1360
- */
1361
- instrumentLuvio(context) {
1362
- instrumentLuvio(context);
1363
- if (this.isRefreshAdapterEvent(context)) {
1364
- this.aggregateRefreshAdapterEvents(context);
1365
- }
1366
- else if (this.isAdapterUnfulfilledError(context)) {
1367
- this.incrementAdapterRequestErrorCount(context);
1368
- }
1369
- else ;
1785
+ buildConcreteRequest(similarRequest, _context) {
1786
+ return similarRequest;
1370
1787
  }
1371
- /**
1372
- * Returns whether or not this is a RefreshAdapterEvent.
1373
- * @param context The transaction context.
1374
- * @returns Whether or not this is a RefreshAdapterEvent.
1375
- */
1376
- isRefreshAdapterEvent(context) {
1377
- return context[REFRESH_ADAPTER_EVENT] === true;
1788
+ canCombine(reqA, reqB) {
1789
+ return reqA.parentObjectApiName === reqB.parentObjectApiName;
1378
1790
  }
1379
- /**
1380
- * Returns whether or not this is an AdapterUnfulfilledError.
1381
- * @param context The transaction context.
1382
- * @returns Whether or not this is an AdapterUnfulfilledError.
1383
- */
1384
- isAdapterUnfulfilledError(context) {
1385
- return context[ADAPTER_UNFULFILLED_ERROR] === true;
1791
+ combineRequests(reqA, reqB) {
1792
+ const combined = { ...reqA };
1793
+ combined.relatedListNames = Array.from(new Set([...reqA.relatedListNames, ...reqB.relatedListNames]));
1794
+ return combined;
1386
1795
  }
1387
- /**
1388
- * Specific instrumentation for getRecordNotifyChange.
1389
- * temporary implementation to match existing aura call for now
1390
- *
1391
- * @param uniqueWeakEtags whether weakEtags match or not
1392
- * @param error if dispatchResourceRequest fails for any reason
1393
- */
1394
- notifyChangeNetwork(uniqueWeakEtags, error) {
1395
- perfStart(NETWORK_TRANSACTION_NAME);
1396
- if (error === true) {
1397
- perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': 'error' });
1398
- }
1399
- else {
1400
- perfEnd(NETWORK_TRANSACTION_NAME, { 'notify-change-network': uniqueWeakEtags });
1401
- }
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;
1402
1804
  }
1403
- /**
1404
- * Parses and aggregates weakETagZero events to be sent in summarized log line.
1405
- * @param context The transaction context.
1406
- */
1407
- aggregateWeakETagEvents(incomingWeakEtagZero, existingWeakEtagZero, apiName) {
1408
- const key = 'weaketag-0-' + apiName;
1409
- if (this.weakEtagZeroEvents[key] === undefined) {
1410
- this.weakEtagZeroEvents[key] = {
1411
- [EXISTING_WEAKETAG_0_KEY]: 0,
1412
- [INCOMING_WEAKETAG_0_KEY]: 0,
1805
+ buildConcreteRequest(similarRequest, context) {
1806
+ return {
1807
+ ...similarRequest,
1808
+ config: {
1809
+ ...similarRequest.config,
1810
+ parentRecordId: context.recordId,
1811
+ },
1812
+ };
1813
+ }
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,
1413
1825
  };
1414
1826
  }
1415
- if (existingWeakEtagZero) {
1416
- this.weakEtagZeroEvents[key][EXISTING_WEAKETAG_0_KEY] += 1;
1417
- }
1418
- if (incomingWeakEtagZero) {
1419
- this.weakEtagZeroEvents[key][INCOMING_WEAKETAG_0_KEY] += 1;
1420
- }
1827
+ return {
1828
+ request: this.transformForSave(request),
1829
+ context,
1830
+ };
1421
1831
  }
1422
- /**
1423
- * Aggregates refresh adapter events to be sent in summarized log line.
1424
- * - how many times refreshApex is called
1425
- * - how many times refresh from lightning/uiRecordApi is called
1426
- * - number of supported calls: refreshApex called on apex adapter
1427
- * - number of unsupported calls: refreshApex on non-apex adapter
1428
- * + any use of refresh from uiRecordApi module
1429
- * - count of refresh calls per adapter
1430
- * @param context The refresh adapter event.
1431
- */
1432
- aggregateRefreshAdapterEvents(context) {
1433
- // We are consolidating all apex adapter instrumentation calls under a single key
1434
- // Adding additional logging that getApex adapters can invoke? Read normalizeAdapterName ts-doc.
1435
- const adapterName = normalizeAdapterName(context.adapterName);
1436
- if (this.lastRefreshApiCall === REFRESH_APEX_KEY) {
1437
- if (isApexAdapter(adapterName)) {
1438
- this.refreshApiCallEventStats[SUPPORTED_KEY] += 1;
1439
- }
1440
- else {
1441
- this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
1442
- }
1443
- }
1444
- else if (this.lastRefreshApiCall === REFRESH_UIAPI_KEY) {
1445
- this.refreshApiCallEventStats[UNSUPPORTED_KEY] += 1;
1446
- }
1447
- if (this.refreshAdapterEvents[adapterName] === undefined) {
1448
- this.refreshAdapterEvents[adapterName] = 0;
1449
- }
1450
- this.refreshAdapterEvents[adapterName] += 1;
1451
- this.lastRefreshApiCall = null;
1832
+ isContextDependent(context, request) {
1833
+ return context.recordId === request.config.parentRecordId;
1452
1834
  }
1453
1835
  /**
1454
- * Increments call stat for incoming refresh api call, and sets the name
1455
- * to be used in {@link aggregateRefreshCalls}
1456
- * @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.
1457
1840
  */
1458
- handleRefreshApiCall(apiName) {
1459
- this.refreshApiCallEventStats[apiName] += 1;
1460
- // set function call to be used with aggregateRefreshCalls
1461
- this.lastRefreshApiCall = apiName;
1841
+ canCombine(reqA, reqB) {
1842
+ return reqA.parentRecordId === reqB.parentRecordId;
1462
1843
  }
1463
1844
  /**
1464
- * W-7302241
1465
- * 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.
1466
1849
  */
1467
- logRefreshStats() {
1468
- if (keys(this.refreshAdapterEvents).length > 0) {
1469
- interaction(REFRESH_PAYLOAD_TARGET, REFRESH_PAYLOAD_SCOPE, this.refreshAdapterEvents, REFRESH_EVENTSOURCE, REFRESH_EVENTTYPE, this.refreshApiCallEventStats);
1470
- this.resetRefreshStats();
1471
- }
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;
1472
1859
  }
1473
- /**
1474
- * Resets the stat trackers for refresh call events.
1475
- */
1476
- resetRefreshStats() {
1477
- this.refreshAdapterEvents = {};
1478
- this.refreshApiCallEventStats = {
1479
- [REFRESH_APEX_KEY]: 0,
1480
- [REFRESH_UIAPI_KEY]: 0,
1481
- [SUPPORTED_KEY]: 0,
1482
- [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
+ },
1483
1875
  };
1484
- this.lastRefreshApiCall = null;
1485
1876
  }
1486
1877
  /**
1487
- * W-7801618
1488
- * Counter for occurrences where the incoming record to be merged has a different apiName.
1489
- * Dynamically generated metric, stored in an {@link RecordApiNameChangeCounters} object.
1490
1878
  *
1491
- * @param context The transaction context.
1879
+ * This method returns GetRelatedListRecordsRequest[] that won't be part of a batch request.
1492
1880
  *
1493
- * 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.
1494
1889
  */
1495
- incrementRecordApiNameChangeCount(_incomingApiName, existingApiName) {
1496
- let apiNameChangeCounter = this.recordApiNameChangeCounters[existingApiName];
1497
- if (apiNameChangeCounter === undefined) {
1498
- apiNameChangeCounter = counter(createMetricsKey(NAMESPACE, RECORD_API_NAME_CHANGE_COUNT_METRIC_NAME, existingApiName));
1499
- 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
+ };
1500
1921
  }
1501
- 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
+ };
1502
1947
  }
1503
- /**
1504
- * W-8620679
1505
- * Increment the counter for an UnfulfilledSnapshotError coming from luvio
1506
- *
1507
- * @param context The transaction context.
1508
- */
1509
- incrementAdapterRequestErrorCount(context) {
1510
- // We are consolidating all apex adapter instrumentation calls under a single key
1511
- const adapterName = normalizeAdapterName(context.adapterName);
1512
- let adapterRequestErrorCounter = this.adapterUnfulfilledErrorCounters[adapterName];
1513
- if (adapterRequestErrorCounter === undefined) {
1514
- adapterRequestErrorCounter = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, ADAPTER_ERROR_COUNT_METRIC_NAME, adapterName));
1515
- 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();
1516
1957
  }
1517
- adapterRequestErrorCounter.increment(1);
1518
- totalAdapterErrorMetric.increment(1);
1958
+ return Promise.resolve(undefined);
1519
1959
  }
1520
1960
  }
1521
- function createMetricsKey(owner, name, unit) {
1522
- let metricName = name;
1523
- if (unit) {
1524
- metricName = metricName + '.' + unit;
1961
+
1962
+ class InMemoryPrefetchStorage {
1963
+ constructor() {
1964
+ this.data = {};
1525
1965
  }
1526
- return {
1527
- get() {
1528
- return { owner: owner, name: metricName };
1529
- },
1530
- };
1531
- }
1532
- /**
1533
- * Returns whether adapter is an Apex one or not.
1534
- * @param adapterName The name of the adapter.
1535
- */
1536
- function isApexAdapter(adapterName) {
1537
- return adapterName.indexOf(APEX_ADAPTER_NAME) > -1;
1538
- }
1539
- /**
1540
- * Normalizes getApex adapter names to `Apex.getApex`. Non-Apex adapters will be prefixed with
1541
- * API family, if supplied. Example: `UiApi.getRecord`.
1542
- *
1543
- * Note: If you are adding additional logging that can come from getApex adapter contexts that provide
1544
- * the full getApex adapter name (i.e. getApex_[namespace]_[class]_[function]_[continuation]),
1545
- * ensure to call this method to normalize all logging to 'getApex'. This
1546
- * is because Argus has a 50k key cardinality limit. More context: W-8379680.
1547
- *
1548
- * @param adapterName The name of the adapter.
1549
- * @param apiFamily The API family of the adapter.
1550
- */
1551
- function normalizeAdapterName(adapterName, apiFamily) {
1552
- if (isApexAdapter(adapterName)) {
1553
- return NORMALIZED_APEX_ADAPTER_NAME;
1966
+ set(key, value) {
1967
+ this.data[key] = value;
1968
+ return Promise.resolve();
1554
1969
  }
1555
- return apiFamily ? `${apiFamily}.${adapterName}` : adapterName;
1556
- }
1557
- const timerMetricTracker = create(null);
1558
- /**
1559
- * Calls instrumentation/service telemetry timer
1560
- * @param name Name of the metric
1561
- * @param duration number to update backing percentile histogram, negative numbers ignored
1562
- */
1563
- function updateTimerMetric(name, duration) {
1564
- let metric = timerMetricTracker[name];
1565
- if (metric === undefined) {
1566
- metric = timer(createMetricsKey(NAMESPACE, name));
1567
- timerMetricTracker[name] = metric;
1970
+ get(key) {
1971
+ return this.data[key];
1568
1972
  }
1569
- timerMetricAddDuration(metric, duration);
1570
1973
  }
1571
- function timerMetricAddDuration(timer, duration) {
1572
- // Guard against negative values since it causes error to be thrown by MetricsService
1573
- if (duration >= 0) {
1574
- 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;
1575
1994
  }
1995
+ return new AuraPrefetchStorage(auraStorage, inMemoryStorage);
1576
1996
  }
1577
- /**
1578
- * W-10315098
1579
- * Increments the counter associated with the request response. Counts are bucketed by status.
1580
- */
1581
- const requestResponseMetricTracker = create(null);
1582
- function incrementRequestResponseCount(cb) {
1583
- const status = cb().status;
1584
- let metric = requestResponseMetricTracker[status];
1585
- if (metric === undefined) {
1586
- metric = counter(createMetricsKey(OBSERVABILITY_NAMESPACE, NETWORK_ADAPTER_RESPONSE_METRIC_NAME, `${status.valueOf()}`));
1587
- 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
+ });
1588
2016
  }
1589
- metric.increment();
1590
- }
1591
- function logObjectInfoChanged() {
1592
- logObjectInfoChanged$1();
1593
- }
1594
- /**
1595
- * Create a new instrumentation cache stats and return it.
1596
- *
1597
- * @param name The cache logger name.
1598
- */
1599
- function registerLdsCacheStats(name) {
1600
- return registerCacheStats(`${NAMESPACE}:${name}`);
1601
- }
1602
- /**
1603
- * Add or overwrite hooks that require aura implementations
1604
- */
1605
- function setAuraInstrumentationHooks() {
1606
- instrument({
1607
- recordConflictsResolved: (serverRequestCount) => {
1608
- // Ignore 0 values which can originate from ADS bridge
1609
- if (serverRequestCount > 0) {
1610
- updatePercentileHistogramMetric('record-conflicts-resolved', serverRequestCount);
1611
- }
1612
- },
1613
- nullDisplayValueConflict: ({ fieldType, areValuesEqual }) => {
1614
- const metricName = `merge-null-dv-count.${fieldType}`;
1615
- if (fieldType === 'scalar') {
1616
- incrementCounterMetric(`${metricName}.${areValuesEqual}`);
1617
- }
1618
- else {
1619
- 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);
1620
2023
  }
1621
- },
1622
- getRecordNotifyChangeAllowed: incrementGetRecordNotifyChangeAllowCount,
1623
- getRecordNotifyChangeDropped: incrementGetRecordNotifyChangeDropCount,
1624
- notifyRecordUpdateAvailableAllowed: incrementNotifyRecordUpdateAvailableAllowCount,
1625
- notifyRecordUpdateAvailableDropped: incrementNotifyRecordUpdateAvailableDropCount,
1626
- recordApiNameChanged: instrumentation.incrementRecordApiNameChangeCount.bind(instrumentation),
1627
- weakEtagZero: instrumentation.aggregateWeakETagEvents.bind(instrumentation),
1628
- getRecordNotifyChangeNetworkResult: instrumentation.notifyChangeNetwork.bind(instrumentation),
1629
- });
1630
- withRegistration('@salesforce/lds-adapters-uiapi', (reg) => setLdsAdaptersUiapiInstrumentation(reg));
1631
- instrument$1({
1632
- logCrud: logCRUDLightningInteraction,
1633
- networkResponse: incrementRequestResponseCount,
1634
- });
1635
- instrument$2({
1636
- refreshCalled: instrumentation.handleRefreshApiCall.bind(instrumentation),
1637
- instrumentAdapter: instrumentation.instrumentAdapter.bind(instrumentation),
1638
- });
1639
- instrument$3({
1640
- timerMetricAddDuration: updateTimerMetric,
1641
- });
1642
- // Our getRecord through aggregate-ui CRUD logging has moved
1643
- // to lds-network-adapter. We still need to respect the
1644
- // orgs environment setting
1645
- if (forceRecordTransactionsDisabled === false) {
1646
- ldsNetworkAdapterInstrument({
1647
- getRecordAggregateResolve: (cb) => {
1648
- const { recordId, apiName } = cb();
1649
- logCRUDLightningInteraction('read', {
1650
- recordId,
1651
- recordType: apiName,
1652
- state: 'SUCCESS',
1653
- });
1654
- },
1655
- getRecordAggregateReject: (cb) => {
1656
- const recordId = cb();
1657
- logCRUDLightningInteraction('read', {
1658
- recordId,
1659
- state: 'ERROR',
1660
- });
1661
- },
1662
2024
  });
2025
+ return inMemoryResult;
2026
+ }
2027
+ get(key) {
2028
+ // we never read from the AuraStorage, except in construction.
2029
+ return this.inMemoryStorage.get(key);
1663
2030
  }
1664
- withRegistration('@salesforce/lds-network-adapter', (reg) => setLdsNetworkAdapterInstrumentation(reg));
1665
- }
1666
- /**
1667
- * Initialize the instrumentation and instrument the LDS instance and the InMemoryStore.
1668
- *
1669
- * @param luvio The Luvio instance to instrument.
1670
- * @param store The InMemoryStore to instrument.
1671
- */
1672
- function setupInstrumentation(luvio, store) {
1673
- setupInstrumentation$1(luvio, store);
1674
- setAuraInstrumentationHooks();
1675
- }
1676
- /**
1677
- * Note: locator.scope is set to 'force_record' in order for the instrumentation gate to work, which will
1678
- * disable all crud operations if it is on.
1679
- * @param eventSource - Source of the logging event.
1680
- * @param attributes - Free form object of attributes to log.
1681
- */
1682
- function logCRUDLightningInteraction(eventSource, attributes) {
1683
- interaction(eventSource, 'force_record', null, eventSource, 'crud', attributes);
1684
2031
  }
1685
- const instrumentation = new Instrumentation();
1686
2032
 
1687
2033
  class NoComposedAdapterTypeError extends TypeError {
1688
2034
  constructor(message, resourceRequest) {
@@ -1900,7 +2246,7 @@ function buildPredictorForContext(context) {
1900
2246
  });
1901
2247
  },
1902
2248
  runPredictions() {
1903
- return __lexPrefetcher.predict();
2249
+ return executeAsyncActivity(METRIC_KEYS.PREDICTIVE_DATA_LOADING_PREDICT, (_act) => __lexPrefetcher.predict());
1904
2250
  },
1905
2251
  getPredictionSummary() {
1906
2252
  return __lexPrefetcher.getPredictionSummary();
@@ -1912,7 +2258,7 @@ function initializeLDS() {
1912
2258
  const storeOptions = {
1913
2259
  scheduler: () => { },
1914
2260
  };
1915
- const store = new InMemoryStore(storeOptions);
2261
+ const store = new InMemoryStore$1(storeOptions);
1916
2262
  const environment = new Environment(store, composedNetworkAdapter);
1917
2263
  const luvio = new Luvio(environment, {
1918
2264
  instrument: instrumentation.instrumentLuvio.bind(instrumentation),
@@ -1926,11 +2272,46 @@ function initializeLDS() {
1926
2272
  setupPredictivePrefetcher(luvio);
1927
2273
  }
1928
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
+ }
1929
2307
  // service function to be invoked by Aura
1930
2308
  function ldsEngineCreator() {
1931
2309
  initializeLDS();
2310
+ if (oneStoreEnabled.isOpen({ fallback: false })) {
2311
+ initializeOneStore();
2312
+ }
1932
2313
  return { name: 'ldsEngineCreator' };
1933
2314
  }
1934
2315
 
1935
- export { buildPredictorForContext, ldsEngineCreator as default, initializeLDS };
1936
- // version: 1.279.0-21eae5cb0
2316
+ export { buildPredictorForContext, ldsEngineCreator as default, initializeLDS, initializeOneStore };
2317
+ // version: 1.280.0-92c104b03