@salesforce/lds-ads-bridge 0.1.0-dev1

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.
@@ -0,0 +1,554 @@
1
+ import type { Luvio, ProxyGraphNode, GraphNode, ResourceIngest } from '@luvio/engine';
2
+ import type {
3
+ ObjectInfoRepresentation,
4
+ FieldValueRepresentation,
5
+ FieldValueRepresentationNormalized,
6
+ RecordRepresentation,
7
+ RecordRepresentationNormalized,
8
+ } from '@salesforce/lds-adapters-uiapi';
9
+ import type {
10
+ RecordRepresentation as DenormalizedRecordRepresentation,
11
+ RecordRepresentationNormalized as DenormalizedRecordRepresentationNormalized,
12
+ } from '@salesforce/lds-runtime-mobile';
13
+ import {
14
+ keyBuilderRecord,
15
+ keyBuilderObjectInfo,
16
+ ingestRecord as ingestNormalizedRecordRepresentation,
17
+ } from '@salesforce/lds-adapters-uiapi';
18
+
19
+ import {
20
+ ArrayPrototypePush,
21
+ JSONParse,
22
+ JSONStringify,
23
+ ObjectKeys,
24
+ ObjectPrototypeHasOwnProperty,
25
+ } from './utils/language';
26
+
27
+ import {
28
+ ADS_BRIDGE_ADD_RECORDS_DURATION,
29
+ ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION,
30
+ ADS_BRIDGE_EVICT_DURATION,
31
+ } from './utils/metric-keys';
32
+ import { instrumentation } from './instrumentation';
33
+
34
+ // No need to pass the actual record key `luvio.ingestStore`. The `RecordRepresentation.ts#ingest`
35
+ // function extracts the appropriate record id from the ingested record.
36
+ const INGEST_KEY = '';
37
+
38
+ const MAIN_RECORD_TYPE_ID = '012000000000000AAA';
39
+
40
+ const DMO_API_NAME_SUFFIX = '__dlm';
41
+
42
+ const API_NAMESPACE = 'UiApi';
43
+
44
+ const RECORD_REPRESENTATION_NAME = 'RecordRepresentation';
45
+
46
+ const RECORD_ID_PREFIX = `${API_NAMESPACE}::${RECORD_REPRESENTATION_NAME}:`;
47
+
48
+ const RECORD_FIELDS_KEY_JUNCTION = '__fields__';
49
+
50
+ type Unsubscribe = () => void;
51
+
52
+ interface AdsRecord {
53
+ /**
54
+ * True if the passed record is a primary record, otherwise false.
55
+ *
56
+ * The Salesforce APIs may represent different records with the same record id. All the
57
+ * records returned by the UI API are primary records.
58
+ */
59
+ isPrimary: boolean;
60
+
61
+ /** The actual record data */
62
+ record: RecordRepresentation;
63
+ }
64
+
65
+ interface AdsRecordMap {
66
+ [recordId: string]: {
67
+ [objectApiName: string]: AdsRecord;
68
+ };
69
+ }
70
+
71
+ interface ObjectMetadata {
72
+ /**
73
+ * The entity key prefix.
74
+ * This originally was typed as simply a "string",
75
+ * however, "keyPrefix" can be null on ObjectInfoRepresentation
76
+ * and existing behavior simply passes ObjectInfoRepresentation.keyPrefix
77
+ * straight to ADS. This type has been updated to capture that
78
+ * a string or null can be passed here.
79
+ * */
80
+
81
+ _keyPrefix: string | null;
82
+
83
+ /** The entity field name. */
84
+ _nameField: string;
85
+
86
+ /** The entity label. */
87
+ _entityLabel: string;
88
+ }
89
+
90
+ interface AdsObjectMetadataMap {
91
+ [objectApiName: string]: ObjectMetadata;
92
+ }
93
+
94
+ type LdsRecordChangedCallback = (record: AdsRecordMap, objectMetadata: AdsObjectMetadataMap) => any;
95
+
96
+ function isGraphNode<T, U>(node: ProxyGraphNode<T, U>): node is GraphNode<T, U> {
97
+ return node !== null && node.type === 'Node';
98
+ }
99
+
100
+ function isSpanningRecord(
101
+ fieldValue: null | string | string[] | number | boolean | RecordRepresentation
102
+ ): fieldValue is RecordRepresentation {
103
+ return fieldValue !== null && typeof fieldValue === 'object' && !Array.isArray(fieldValue);
104
+ }
105
+
106
+ function isStoreKeyRecordId(key: string) {
107
+ return key.indexOf(RECORD_ID_PREFIX) > -1 && key.indexOf(RECORD_FIELDS_KEY_JUNCTION) === -1;
108
+ }
109
+
110
+ /**
111
+ * Returns a shallow copy of a record with its field values if it is a scalar and a reference and a
112
+ * a RecordRepresentation with no field if the value if a spanning record.
113
+ * It returns null if the record contains any pending field.
114
+ */
115
+ function getShallowRecordDenormalized(
116
+ luvio: Luvio,
117
+ storeRecordId: string
118
+ ): DenormalizedRecordRepresentation | null {
119
+ const recordNode = luvio.getNode<
120
+ DenormalizedRecordRepresentationNormalized,
121
+ DenormalizedRecordRepresentation
122
+ >(storeRecordId);
123
+
124
+ if (!isGraphNode(recordNode)) {
125
+ return null;
126
+ }
127
+
128
+ const fieldsCopy: DenormalizedRecordRepresentation['fields'] = {};
129
+ const copy: DenormalizedRecordRepresentation = {
130
+ ...recordNode.retrieve(),
131
+ fields: fieldsCopy,
132
+ childRelationships: {},
133
+ };
134
+
135
+ const fieldsNode = recordNode.object('fields');
136
+ const fieldNames = fieldsNode.keys();
137
+
138
+ for (let i = 0, len = fieldNames.length; i < len; i++) {
139
+ let fieldCopy: FieldValueRepresentation;
140
+
141
+ const fieldName = fieldNames[i];
142
+ if (fieldsNode.isPending(fieldName) === true) {
143
+ return null;
144
+ }
145
+
146
+ if (fieldsNode.isMissing(fieldName) === true) {
147
+ continue;
148
+ }
149
+
150
+ const fieldObject = fieldsNode.object(fieldName);
151
+ const { displayValue, value } = fieldObject.retrieve();
152
+ if (fieldObject.isScalar('value') || Array.isArray(fieldObject.data?.value)) {
153
+ fieldCopy = {
154
+ displayValue: displayValue,
155
+ value: value as string | number | boolean | string[] | null,
156
+ };
157
+ } else {
158
+ const spanningRecordLink = fieldObject.link<
159
+ DenormalizedRecordRepresentationNormalized,
160
+ DenormalizedRecordRepresentation
161
+ >('value');
162
+ if (spanningRecordLink.isPending() === true) {
163
+ return null;
164
+ }
165
+
166
+ const spanningRecordNode = spanningRecordLink.follow();
167
+ if (!isGraphNode(spanningRecordNode)) {
168
+ continue;
169
+ }
170
+
171
+ fieldCopy = {
172
+ displayValue,
173
+ value: {
174
+ ...spanningRecordNode.retrieve(),
175
+ fields: {},
176
+ childRelationships: {},
177
+ },
178
+ };
179
+ }
180
+
181
+ fieldsCopy[fieldName] = fieldCopy;
182
+ }
183
+ return copy;
184
+ }
185
+
186
+ /**
187
+ * Returns a shallow copy of a record with its field values if it is a scalar and a reference and a
188
+ * a RecordRepresentation with no field if the value if a spanning record.
189
+ * It returns null if the record contains any pending field.
190
+ */
191
+ function getShallowRecord(luvio: Luvio, storeRecordId: string): RecordRepresentation | null {
192
+ const recordNode = luvio.getNode<RecordRepresentationNormalized, RecordRepresentation>(
193
+ storeRecordId
194
+ );
195
+
196
+ if (!isGraphNode(recordNode)) {
197
+ return null;
198
+ }
199
+
200
+ const fieldsCopy: RecordRepresentation['fields'] = {};
201
+ const copy: RecordRepresentation = {
202
+ ...recordNode.retrieve(),
203
+ fields: fieldsCopy,
204
+ childRelationships: {},
205
+ };
206
+
207
+ const fieldsNode = recordNode.object('fields');
208
+ const fieldNames = fieldsNode.keys();
209
+
210
+ for (let i = 0, len = fieldNames.length; i < len; i++) {
211
+ let fieldCopy: FieldValueRepresentation;
212
+
213
+ const fieldName = fieldNames[i];
214
+ const fieldLink = fieldsNode.link<
215
+ FieldValueRepresentationNormalized,
216
+ FieldValueRepresentation
217
+ >(fieldName);
218
+ if (fieldLink.isPending() === true) {
219
+ return null;
220
+ }
221
+
222
+ const fieldNode = fieldLink.follow();
223
+ if (!isGraphNode(fieldNode)) {
224
+ continue;
225
+ }
226
+
227
+ const { displayValue, value } = fieldNode.retrieve();
228
+ if (fieldNode.isScalar('value') || Array.isArray(fieldNode.data?.value)) {
229
+ fieldCopy = {
230
+ displayValue: displayValue,
231
+ value: value as string | number | boolean | string[] | null,
232
+ };
233
+ } else {
234
+ const spanningRecordLink = fieldNode.link<
235
+ RecordRepresentationNormalized,
236
+ RecordRepresentation
237
+ >('value');
238
+ if (spanningRecordLink.isPending() === true) {
239
+ return null;
240
+ }
241
+
242
+ const spanningRecordNode = spanningRecordLink.follow();
243
+ if (!isGraphNode(spanningRecordNode)) {
244
+ continue;
245
+ }
246
+
247
+ fieldCopy = {
248
+ displayValue,
249
+ value: {
250
+ ...spanningRecordNode.retrieve(),
251
+ fields: {},
252
+ childRelationships: {},
253
+ },
254
+ };
255
+ }
256
+
257
+ fieldsCopy[fieldName] = fieldCopy;
258
+ }
259
+ return copy;
260
+ }
261
+
262
+ /**
263
+ * Returns the ADS object metadata representation for a specific record.
264
+ */
265
+ function getObjectMetadata(luvio: Luvio, record: RecordRepresentation): ObjectMetadata {
266
+ const { data: objectInfo } = luvio.storeLookup<ObjectInfoRepresentation>({
267
+ recordId: keyBuilderObjectInfo(luvio, { apiName: record.apiName }),
268
+ node: {
269
+ kind: 'Fragment',
270
+ private: ['eTag'],
271
+ opaque: true,
272
+ },
273
+ variables: {},
274
+ });
275
+
276
+ if (objectInfo !== undefined) {
277
+ let nameField = 'Name';
278
+
279
+ // Extract the entity name field from the object info. In the case where there are multiple
280
+ // field names then pick up the first one.
281
+ if (objectInfo.nameFields.length !== 0 && objectInfo.nameFields.indexOf('Name') === -1) {
282
+ nameField = objectInfo.nameFields[0];
283
+ }
284
+
285
+ return {
286
+ _nameField: nameField,
287
+ _entityLabel: objectInfo.label,
288
+ _keyPrefix: objectInfo.keyPrefix,
289
+ };
290
+ }
291
+
292
+ return {
293
+ _nameField: 'Name',
294
+ _entityLabel: record.apiName,
295
+ _keyPrefix: record.id.substring(0, 3),
296
+ };
297
+ }
298
+
299
+ /**
300
+ * RecordGvp can send records back to ADS with record types incorrectly set to the master
301
+ * record type. Since there are no known legitimate scenarios where a record can change from a
302
+ * non-master record type back to the master record type, we assume such a transition
303
+ * indicates a RecordGvp mistake. This function checks for that scenario and overwrites the
304
+ * incoming ADS record type information with what we already have in the store when it
305
+ * occurs.
306
+ *
307
+ * @param luvio Luvio
308
+ * @param record record from ADS, will be fixed in situ
309
+ */
310
+ function fixRecordTypes(luvio: Luvio, record: RecordRepresentation): void {
311
+ // non-master record types should always be correct
312
+ if (record.recordTypeId === MAIN_RECORD_TYPE_ID) {
313
+ const key = keyBuilderRecord(luvio, { recordId: record.id });
314
+ const recordNode = luvio.getNode<RecordRepresentationNormalized, RecordRepresentation>(key);
315
+
316
+ if (isGraphNode(recordNode) && recordNode.scalar('recordTypeId') !== MAIN_RECORD_TYPE_ID) {
317
+ // ignore bogus incoming record type information & keep what we have
318
+ record.recordTypeId = recordNode.scalar('recordTypeId');
319
+ record.recordTypeInfo = recordNode.object('recordTypeInfo').data;
320
+ }
321
+ }
322
+
323
+ // recurse on nested records
324
+ const fieldKeys = ObjectKeys(record.fields);
325
+ const fieldKeysLen = fieldKeys.length;
326
+ for (let i = 0; i < fieldKeysLen; ++i) {
327
+ const fieldValue = record.fields[fieldKeys[i]].value;
328
+ if (isSpanningRecord(fieldValue)) {
329
+ fixRecordTypes(luvio, fieldValue);
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Returns whether or not a the record is a DMO entity.
336
+ * @param record - The record.
337
+ * @returns True if DMO, false otherwise.
338
+ */
339
+ export function isDMOEntity(record: RecordRepresentation): boolean {
340
+ return record.apiName.endsWith(DMO_API_NAME_SUFFIX);
341
+ }
342
+
343
+ export default class AdsBridge {
344
+ private isRecordEmitLocked: boolean = false;
345
+ private watchUnsubscribe: Unsubscribe | undefined;
346
+
347
+ constructor(
348
+ private luvio: Luvio,
349
+ private recordRepresentationIngestOverride: ResourceIngest | undefined
350
+ ) {}
351
+
352
+ /**
353
+ * This setter invoked by recordLibrary to listen for records ingested by Luvio. The passed method
354
+ * is invoked whenever a record is ingested. It may be via getRecord, getRecordUi, getListUi, ...
355
+ */
356
+ public set receiveFromLdsCallback(callback: LdsRecordChangedCallback | undefined) {
357
+ // Unsubscribe if there is an existing subscription.
358
+ if (this.watchUnsubscribe !== undefined) {
359
+ this.watchUnsubscribe();
360
+ this.watchUnsubscribe = undefined;
361
+ }
362
+
363
+ if (callback !== undefined) {
364
+ this.watchUnsubscribe = this.luvio.storeWatch(RECORD_ID_PREFIX, (entries) => {
365
+ if (this.isRecordEmitLocked === true) {
366
+ return;
367
+ }
368
+
369
+ this.emitRecordChanged(entries, callback);
370
+ });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * This method is invoked when a record has been ingested by ADS.
376
+ *
377
+ * ADS may invoke this method with records that are not UIAPI allowlisted so not refreshable by
378
+ * Luvio. Luvio filters the provided list so it ingests only UIAPI allowlisted records.
379
+ */
380
+ public addRecords(
381
+ records: RecordRepresentation[],
382
+ uiApiEntityAllowlist?: {
383
+ [name: string]: 'false' | undefined;
384
+ }
385
+ ): void {
386
+ const startTime = Date.now();
387
+ const { luvio } = this;
388
+ let didIngestRecord = false;
389
+ return this.lockLdsRecordEmit(() => {
390
+ for (let i = 0; i < records.length; i++) {
391
+ const record = records[i];
392
+ const { apiName } = record;
393
+
394
+ // Ingest the record if no allowlist is passed or the entity name is allowlisted.
395
+ if (
396
+ uiApiEntityAllowlist === undefined ||
397
+ uiApiEntityAllowlist[apiName] !== 'false'
398
+ ) {
399
+ didIngestRecord = true;
400
+
401
+ // Deep-copy the record to ingest and ingest the record copy. This avoids
402
+ // corrupting the ADS cache since ingestion mutates the passed record.
403
+ const recordCopy = JSONParse(JSONStringify(record));
404
+
405
+ // Don't let incorrect ADS/RecordGVP recordTypeIds replace a valid record type in our store
406
+ // with the master record type. See W-7302870 for details.
407
+ fixRecordTypes(luvio, recordCopy);
408
+
409
+ const recordIngest =
410
+ this.recordRepresentationIngestOverride !== undefined
411
+ ? this.recordRepresentationIngestOverride
412
+ : ingestNormalizedRecordRepresentation;
413
+
414
+ luvio.storeIngest(INGEST_KEY, recordIngest, recordCopy);
415
+ }
416
+ }
417
+
418
+ if (didIngestRecord === true) {
419
+ luvio.storeBroadcast();
420
+ }
421
+ instrumentation.timerMetricAddDuration(
422
+ ADS_BRIDGE_ADD_RECORDS_DURATION,
423
+ Date.now() - startTime
424
+ );
425
+ });
426
+ }
427
+
428
+ /**
429
+ * This method is invoked whenever a record has been evicted from ADS.
430
+ */
431
+ public evict(recordId: string): Promise<void> {
432
+ const startTime = Date.now();
433
+ const { luvio } = this;
434
+ const key = keyBuilderRecord(luvio, { recordId });
435
+ return this.lockLdsRecordEmit(() => {
436
+ luvio.storeEvict(key);
437
+ luvio.storeBroadcast();
438
+ instrumentation.timerMetricAddDuration(
439
+ ADS_BRIDGE_EVICT_DURATION,
440
+ Date.now() - startTime
441
+ );
442
+ return Promise.resolve();
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Gets the list of fields of a record that Luvio has in its store. The field list doesn't
448
+ * contains the spanning record fields. ADS uses this list when it loads a record from the
449
+ * server. This is an optimization to make a single roundtrip it queries for all fields required
450
+ * by ADS and Luvio.
451
+ */
452
+ public getTrackedFieldsForRecord(recordId: string): Promise<string[]> {
453
+ const { luvio } = this;
454
+ const storeRecordId = keyBuilderRecord(luvio, { recordId });
455
+
456
+ const recordNode = luvio.getNode<RecordRepresentationNormalized, RecordRepresentation>(
457
+ storeRecordId
458
+ );
459
+
460
+ if (!isGraphNode(recordNode)) {
461
+ return Promise.resolve([]);
462
+ }
463
+
464
+ const apiName = recordNode.scalar('apiName');
465
+ const fieldNames = recordNode.object('fields').keys();
466
+
467
+ // Prefix all the fields with the record API name.
468
+ const qualifiedFieldNames: string[] = [];
469
+ for (let i = 0, len = fieldNames.length; i < len; i++) {
470
+ ArrayPrototypePush.call(qualifiedFieldNames, `${apiName}.${fieldNames[i]}`);
471
+ }
472
+
473
+ return Promise.resolve(qualifiedFieldNames);
474
+ }
475
+
476
+ /**
477
+ * Prevents the bridge to emit record change during the execution of the callback.
478
+ * This methods should wrap all the Luvio store mutation done by the bridge. It prevents Luvio store
479
+ * mutations triggered by ADS to be emit back to ADS.
480
+ */
481
+ private lockLdsRecordEmit<T>(callback: () => T): T {
482
+ this.isRecordEmitLocked = true;
483
+
484
+ try {
485
+ return callback();
486
+ } finally {
487
+ this.isRecordEmitLocked = false;
488
+ }
489
+ }
490
+
491
+ /**
492
+ * This method retrieves queries the store with with passed record ids to retrieve their
493
+ * associated records and object info. Note that the passed ids are not Salesforce record id
494
+ * but rather Luvio internals store ids.
495
+ */
496
+ private emitRecordChanged(
497
+ updatedEntries: { id: string }[],
498
+ callback: LdsRecordChangedCallback
499
+ ): void {
500
+ const startTime = Date.now();
501
+ const { luvio } = this;
502
+ let shouldEmit = false;
503
+
504
+ const adsRecordMap: AdsRecordMap = {};
505
+ const adsObjectMap: AdsObjectMetadataMap = {};
506
+
507
+ for (let i = 0; i < updatedEntries.length; i++) {
508
+ const storeRecordId = updatedEntries[i].id;
509
+
510
+ // Exclude all the store record ids not matching with the record id pattern.
511
+ // Note: FieldValueRepresentation have the same prefix than RecordRepresentation so we
512
+ // need to filter them out.
513
+ if (!isStoreKeyRecordId(storeRecordId)) {
514
+ continue;
515
+ }
516
+
517
+ const record =
518
+ this.recordRepresentationIngestOverride !== undefined
519
+ ? getShallowRecordDenormalized(luvio, storeRecordId)
520
+ : getShallowRecord(luvio, storeRecordId);
521
+ if (record === null) {
522
+ continue;
523
+ }
524
+
525
+ // W-9978523
526
+ if (isDMOEntity(record) === true) {
527
+ continue;
528
+ }
529
+
530
+ const { id, apiName } = record;
531
+
532
+ shouldEmit = true;
533
+ adsRecordMap[id] = {
534
+ [apiName]: {
535
+ isPrimary: true,
536
+ record,
537
+ },
538
+ };
539
+
540
+ // Extract and add the object metadata to the map if not already present.
541
+ if (!ObjectPrototypeHasOwnProperty.call(adsObjectMap, apiName)) {
542
+ adsObjectMap[apiName] = getObjectMetadata(luvio, record);
543
+ }
544
+ }
545
+
546
+ if (shouldEmit === true) {
547
+ callback(adsRecordMap, adsObjectMap);
548
+ }
549
+ instrumentation.timerMetricAddDuration(
550
+ ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION,
551
+ Date.now() - startTime
552
+ );
553
+ }
554
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Instrumentation hooks exposed by this module.
3
+ */
4
+ export interface AdsBridgeInstrumentation {
5
+ /**
6
+ * Called at the end of execution for a function to track latency
7
+ * Current functions tracked: packages/lds-ads-bridge/src/utils/metric-keys.ts
8
+ */
9
+ timerMetricAddDuration?: (metricName: string, valueInMs: number) => void;
10
+ }
11
+
12
+ // For use by callers within this module to instrument interesting things.
13
+ export let instrumentation = {
14
+ timerMetricAddDuration: (_metricName: string, _valueInMs: number) => {},
15
+ };
16
+
17
+ /**
18
+ * Allows external modules (typically a runtime environment) to set
19
+ * instrumentation hooks for this module. Note that the hooks are
20
+ * incremental - hooks not suppiled in newInstrumentation will retain
21
+ * their previous values. The default instrumentation hooks are no-ops.
22
+ *
23
+ * @param newInstrumentation instrumentation hooks to be overridden
24
+ */
25
+ export function instrument(newInstrumentation: AdsBridgeInstrumentation) {
26
+ instrumentation = Object.assign(instrumentation, newInstrumentation);
27
+ }
package/src/main.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { Luvio } from '@luvio/engine';
2
+ import AdsBridge from './ads-bridge';
3
+
4
+ import { getRecordIngestionOverride } from '@salesforce/lds-adapters-uiapi';
5
+ import { withDefaultLuvio } from '@salesforce/lds-default-luvio';
6
+
7
+ /**
8
+ * Callback used to inform interested parties that a new default Luvio has been set.
9
+ */
10
+ export type Callback = (adsBridge: AdsBridge) => void;
11
+
12
+ // most recently created AdsBridge
13
+ let adsBridge: AdsBridge;
14
+
15
+ // callbacks to be invoked when AdsBridge is set/changed
16
+ let callbacks: Callback[] = [];
17
+
18
+ // create a new AdsBridge whenever the default Luvio is set/changed
19
+ withDefaultLuvio((luvio: Luvio) => {
20
+ /**
21
+ * Cache the current value of ingestion override on startup.
22
+ * This needs be set prior to loading of the ADS bridge.
23
+ */
24
+ const recordIngestionOverride = getRecordIngestionOverride();
25
+ adsBridge = new AdsBridge(luvio, recordIngestionOverride);
26
+
27
+ for (let i = 0; i < callbacks.length; ++i) {
28
+ callbacks[i](adsBridge);
29
+ }
30
+ });
31
+
32
+ /**
33
+ * Registers a callback to be invoked with the AdsBridge instance. Note that the
34
+ * callback may be invoked multiple times if the default Luvio changes.
35
+ *
36
+ * @param callback callback to be invoked with the AdsBridge
37
+ */
38
+ export function withAdsBridge(callback: Callback) {
39
+ if (adsBridge) {
40
+ callback(adsBridge);
41
+ }
42
+ callbacks.push(callback);
43
+ }
44
+
45
+ // Expose module instrumentation
46
+ export { instrument, AdsBridgeInstrumentation } from './instrumentation';
@@ -0,0 +1,16 @@
1
+ const { push } = Array.prototype;
2
+ const { keys } = Object;
3
+ const { hasOwnProperty } = Object.prototype;
4
+ const { parse, stringify } = JSON;
5
+
6
+ export {
7
+ // Array.prototype
8
+ push as ArrayPrototypePush,
9
+ // Object
10
+ keys as ObjectKeys,
11
+ // Object.prototype
12
+ hasOwnProperty as ObjectPrototypeHasOwnProperty,
13
+ // JSON
14
+ parse as JSONParse,
15
+ stringify as JSONStringify,
16
+ };
@@ -0,0 +1,3 @@
1
+ export const ADS_BRIDGE_ADD_RECORDS_DURATION = 'ads-bridge-add-records-duration';
2
+ export const ADS_BRIDGE_EMIT_RECORD_CHANGED_DURATION = 'ads-bridge-emit-record-changed-duration';
3
+ export const ADS_BRIDGE_EVICT_DURATION = 'ads-bridge-evict-duration';
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src"]
4
+ }