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