@salesforce/lds-runtime-bridge 1.418.0 → 1.420.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@
12
12
  * *******************************************************************************************
13
13
  */
14
14
  import { setDefaultLuvio } from 'force/ldsEngine';
15
- import { setBypassDeepFreeze, StoreKeySet, serializeStructuredKey, StringKeyInMemoryStore, Reader, deepFreeze, emitAdapterEvent, InMemoryStore, Environment, Luvio } from 'force/luvioEngine';
15
+ import { setBypassDeepFreeze, StoreKeySet, StringKeyInMemoryStore, Reader, serializeStructuredKey, deepFreeze, emitAdapterEvent, InMemoryStore, Environment, Luvio } from 'force/luvioEngine';
16
16
  import { setupInstrumentation, instrumentAdapter as instrumentAdapter$1, instrumentLuvio } from 'force/ldsInstrumentation';
17
17
  import { idleDetector, getInstrumentation } from 'o11y/client';
18
18
  import { instrument } from 'force/ldsBindings';
@@ -43,7 +43,7 @@ const RedirectDurableSegment = 'REDIRECT_KEYS';
43
43
  const MessagingDurableSegment = 'MESSAGING';
44
44
  const MessageNotifyStoreUpdateAvailable = 'notifyStoreUpdateAvailable';
45
45
 
46
- const { keys: keys$2, create: create$2, assign: assign$2, freeze: freeze$1 } = Object;
46
+ const { keys: keys$2, create: create$2, assign: assign$2, freeze: freeze$1, isFrozen } = Object;
47
47
 
48
48
  //Durable store error instrumentation key
49
49
  const DURABLE_STORE_ERROR = 'durable-store-error';
@@ -130,6 +130,74 @@ function publishDurableStoreEntries(durableRecords, put, publishMetadata) {
130
130
  }
131
131
  return { revivedKeys, hadUnexpectedShape };
132
132
  }
133
+ /**
134
+ * Extracts field filtering configuration from the selection.
135
+ */
136
+ function extractFieldFilteringConfig(select, recordId, baseEnvironment) {
137
+ const rootRecordKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(recordId));
138
+ let topLevelFields;
139
+ let nestedFieldRequirements;
140
+ if (select &&
141
+ select.node.kind === 'Fragment' &&
142
+ 'selections' in select.node &&
143
+ select.node.selections) {
144
+ topLevelFields = extractRequestedFieldNames(select.node.selections);
145
+ nestedFieldRequirements = extractNestedFieldRequirements(select.node.selections);
146
+ }
147
+ // Merge all nested field requirements into a single set
148
+ let nestedFields;
149
+ if (nestedFieldRequirements && nestedFieldRequirements.size > 0) {
150
+ nestedFields = new Set();
151
+ for (const fieldSet of nestedFieldRequirements.values()) {
152
+ for (const field of fieldSet) {
153
+ nestedFields.add(field);
154
+ }
155
+ }
156
+ }
157
+ return { rootRecordKey, topLevelFields, nestedFields };
158
+ }
159
+ /**
160
+ * Categorizes keys into different fetch strategies based on filtering requirements.
161
+ */
162
+ function categorizeKeysForL2Fetch(keysToRevive, config, baseEnvironment, shouldFilterFields) {
163
+ const unfilteredKeys = [];
164
+ const rootKeysWithTopLevelFields = [];
165
+ const nestedKeysWithNestedFields = [];
166
+ const canFilter = config.topLevelFields !== undefined && config.topLevelFields.size > 0;
167
+ for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
168
+ const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
169
+ if (!shouldFilterFields(canonicalKey) || !canFilter) {
170
+ unfilteredKeys.push(canonicalKey);
171
+ continue;
172
+ }
173
+ const isRootRecord = canonicalKey === config.rootRecordKey;
174
+ if (isRootRecord) {
175
+ rootKeysWithTopLevelFields.push(canonicalKey);
176
+ }
177
+ else if (config.nestedFields !== undefined && config.nestedFields.size > 0) {
178
+ nestedKeysWithNestedFields.push(canonicalKey);
179
+ }
180
+ else {
181
+ unfilteredKeys.push(canonicalKey);
182
+ }
183
+ }
184
+ return { unfilteredKeys, rootKeysWithTopLevelFields, nestedKeysWithNestedFields };
185
+ }
186
+ /**
187
+ * Builds L2 fetch promises for different key categories.
188
+ */
189
+ function buildL2FetchPromises(categorizedKeys, config, durableStore) {
190
+ const promises = [
191
+ durableStore.getEntries(categorizedKeys.unfilteredKeys, DefaultDurableSegment),
192
+ ];
193
+ if (config.topLevelFields && categorizedKeys.rootKeysWithTopLevelFields.length > 0) {
194
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.rootKeysWithTopLevelFields, config.topLevelFields, DefaultDurableSegment));
195
+ }
196
+ if (config.nestedFields && categorizedKeys.nestedKeysWithNestedFields.length > 0) {
197
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.nestedKeysWithNestedFields, config.nestedFields, DefaultDurableSegment));
198
+ }
199
+ return promises;
200
+ }
133
201
  /**
134
202
  * This method returns a Promise to a snapshot that is revived from L2 cache. If
135
203
  * L2 does not have the entries necessary to fulfill the snapshot then this method
@@ -165,53 +233,41 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
165
233
  const keysToRevive = keysToReviveSet.keysAsArray();
166
234
  const start = Date.now();
167
235
  const { l2Trips } = reviveMetrics;
168
- // Extract requested fields first to determine if filtering is possible
169
- let requestedFields;
170
- if (select.node.kind === 'Fragment' && 'selections' in select.node && select.node.selections) {
171
- requestedFields = extractRequestedFieldNames(select.node.selections);
172
- }
173
- const canonicalKeys = [];
174
- const filteredCanonicalKeys = [];
175
- for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
176
- const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
177
- // Only filter if we have fields to filter and shouldFilterFields returns true
178
- if (requestedFields !== undefined &&
179
- requestedFields.size > 0 &&
180
- shouldFilterFields(canonicalKey)) {
181
- filteredCanonicalKeys.push(canonicalKey);
182
- }
183
- else {
184
- canonicalKeys.push(canonicalKey);
185
- }
186
- }
187
- const fetchPromises = [
188
- durableStore.getEntries(canonicalKeys, DefaultDurableSegment),
189
- ];
190
- if (requestedFields !== undefined &&
191
- requestedFields.size > 0 &&
192
- filteredCanonicalKeys.length > 0) {
193
- fetchPromises.push(durableStore.getEntriesWithSpecificFields(filteredCanonicalKeys, requestedFields, DefaultDurableSegment));
194
- }
195
- return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords]) => {
236
+ // Extract field filtering requirements from the selection
237
+ const fieldFilteringConfig = extractFieldFilteringConfig(select, recordId, baseEnvironment);
238
+ // Categorize keys by how they should be fetched from L2
239
+ const categorizedKeys = categorizeKeysForL2Fetch(keysToRevive, fieldFilteringConfig, baseEnvironment, shouldFilterFields);
240
+ // Build fetch promises for each category
241
+ const fetchPromises = buildL2FetchPromises(categorizedKeys, fieldFilteringConfig, durableStore);
242
+ return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords, nestedFilteredDurableRecords]) => {
243
+ const totalKeysRequested = categorizedKeys.unfilteredKeys.length +
244
+ categorizedKeys.rootKeysWithTopLevelFields.length +
245
+ categorizedKeys.nestedKeysWithNestedFields.length;
196
246
  l2Trips.push({
197
247
  duration: Date.now() - start,
198
- keysRequestedCount: canonicalKeys.length + filteredCanonicalKeys.length,
248
+ keysRequestedCount: totalKeysRequested,
199
249
  });
200
- // Process both normal and filtered records in a single pass
250
+ // Process all three categories of records
201
251
  const revivedKeys = new StoreKeySet();
202
252
  let hadUnexpectedShape = false;
203
- // Process normal records
253
+ // Process normal records (all fields)
204
254
  if (durableRecords !== undefined) {
205
255
  const normalResult = publishDurableStoreEntries(durableRecords, baseEnvironment.storePut.bind(baseEnvironment), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
206
256
  revivedKeys.merge(normalResult.revivedKeys);
207
257
  hadUnexpectedShape = hadUnexpectedShape || normalResult.hadUnexpectedShape;
208
258
  }
209
- // Process filtered records with merging
259
+ // Process filtered records (root with top-level fields) with merging
210
260
  if (filteredDurableRecords !== undefined) {
211
261
  const filteredResult = publishDurableStoreEntries(filteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
212
262
  revivedKeys.merge(filteredResult.revivedKeys);
213
263
  hadUnexpectedShape = hadUnexpectedShape || filteredResult.hadUnexpectedShape;
214
264
  }
265
+ // Process nested filtered records with merging
266
+ if (nestedFilteredDurableRecords !== undefined) {
267
+ const nestedFilteredResult = publishDurableStoreEntries(nestedFilteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
268
+ revivedKeys.merge(nestedFilteredResult.revivedKeys);
269
+ hadUnexpectedShape = hadUnexpectedShape || nestedFilteredResult.hadUnexpectedShape;
270
+ }
215
271
  // if the data coming back from DS had an unexpected shape then just
216
272
  // return the L1 snapshot
217
273
  if (hadUnexpectedShape === true) {
@@ -257,6 +313,54 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
257
313
  return { snapshot: unavailableSnapshot, metrics: reviveMetrics };
258
314
  });
259
315
  }
316
+ /**
317
+ * Filters out null fields from a fields object (null indicates field doesn't exist in L2).
318
+ * Returns a new object without null values, or the original if no filtering needed.
319
+ */
320
+ function filterNullFields(fields) {
321
+ const keys$1 = keys$2(fields);
322
+ let hasNull = false;
323
+ // Check if any nulls exist before allocating new object
324
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
325
+ if (fields[keys$1[i]] === null) {
326
+ hasNull = true;
327
+ break;
328
+ }
329
+ }
330
+ if (!hasNull) {
331
+ return fields;
332
+ }
333
+ const cleaned = {};
334
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
335
+ const key = keys$1[i];
336
+ if (fields[key] !== null) {
337
+ cleaned[key] = fields[key];
338
+ }
339
+ }
340
+ return cleaned;
341
+ }
342
+ /**
343
+ * Merges new fields into existing fields object, skipping null values.
344
+ * Creates a new object to avoid mutations.
345
+ */
346
+ function mergeFieldsObjects(existing, incoming) {
347
+ const merged = { ...existing };
348
+ const keys$1 = keys$2(incoming);
349
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
350
+ const key = keys$1[i];
351
+ // Skip null values - they indicate the field doesn't exist in L2
352
+ if (incoming[key] !== null) {
353
+ merged[key] = incoming[key];
354
+ }
355
+ }
356
+ return merged;
357
+ }
358
+ /**
359
+ * Type guard to check if value is a non-null object.
360
+ */
361
+ function isObject(value) {
362
+ return typeof value === 'object' && value !== null;
363
+ }
260
364
  /**
261
365
  * Creates a put function that merges filtered fields with existing L1 records instead of replacing them.
262
366
  * This is used when reviving filtered fields from L2 to preserve existing data in L1.
@@ -264,28 +368,36 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
264
368
  function createMergeFilteredPut(readEntry, storePut) {
265
369
  return (key, filteredData) => {
266
370
  const existingRecord = readEntry(key);
267
- if (existingRecord !== undefined &&
268
- existingRecord !== null &&
269
- typeof filteredData === 'object' &&
270
- filteredData !== null &&
271
- typeof existingRecord === 'object') {
272
- const filteredFields = filteredData;
273
- const existingObj = existingRecord;
274
- // Check if object is frozen (can happen after deepFreeze)
275
- // If frozen, create a shallow copy to merge fields into
276
- let targetObj = existingObj;
277
- if (Object.isFrozen(existingObj)) {
278
- targetObj = { ...existingObj };
279
- }
280
- const keys = Object.keys(filteredFields);
281
- for (let i = 0, len = keys.length; i < len; i += 1) {
282
- const fieldKey = keys[i];
283
- targetObj[fieldKey] = filteredFields[fieldKey];
371
+ // Merge with existing record if both are objects
372
+ if (isObject(existingRecord) && isObject(filteredData)) {
373
+ // Create target object (copy if frozen to avoid mutation errors)
374
+ const targetObj = isFrozen(existingRecord)
375
+ ? { ...existingRecord }
376
+ : existingRecord;
377
+ const keys$1 = keys$2(filteredData);
378
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
379
+ const fieldKey = keys$1[i];
380
+ const incomingValue = filteredData[fieldKey];
381
+ // Special handling for 'fields' property to merge rather than replace
382
+ if (fieldKey === 'fields' &&
383
+ isObject(incomingValue) &&
384
+ isObject(targetObj[fieldKey])) {
385
+ targetObj[fieldKey] = mergeFieldsObjects(targetObj[fieldKey], incomingValue);
386
+ }
387
+ else {
388
+ targetObj[fieldKey] = incomingValue;
389
+ }
284
390
  }
285
391
  storePut(key, targetObj);
286
392
  }
287
393
  else {
288
- // No existing record, just put the filtered data
394
+ // No existing record - clean null fields before storing
395
+ if (isObject(filteredData) && 'fields' in filteredData) {
396
+ const fields = filteredData.fields;
397
+ if (isObject(fields)) {
398
+ filteredData.fields = filterNullFields(fields);
399
+ }
400
+ }
289
401
  storePut(key, filteredData);
290
402
  }
291
403
  };
@@ -299,16 +411,101 @@ function extractRequestedFieldNames(selections) {
299
411
  return undefined;
300
412
  }
301
413
  // Find the 'fields' ObjectSelection
302
- const fieldsSelection = selections.find((sel) => sel.kind === 'Object' && sel.name === 'fields');
303
- if (!fieldsSelection || !fieldsSelection.selections) {
414
+ let fieldsSelection;
415
+ for (let i = 0, len = selections.length; i < len; i += 1) {
416
+ const sel = selections[i];
417
+ if (sel.kind === 'Object' && sel.name === 'fields') {
418
+ fieldsSelection = sel;
419
+ break;
420
+ }
421
+ }
422
+ if (!fieldsSelection ||
423
+ !fieldsSelection.selections ||
424
+ fieldsSelection.selections.length === 0) {
304
425
  return undefined;
305
426
  }
306
427
  // Extract all field names from the fields selections
428
+ const fieldSelections = fieldsSelection.selections;
307
429
  const fieldNames = new Set();
308
- for (const fieldSel of fieldsSelection.selections) {
309
- fieldNames.add(fieldSel.name);
430
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
431
+ fieldNames.add(fieldSelections[i].name);
432
+ }
433
+ return fieldNames;
434
+ }
435
+ /**
436
+ * Extracts nested field requirements for spanning fields.
437
+ * For spanning fields like Case.CreatedBy.Name, we need to extract what fields
438
+ * are requested from the nested record (User in this case).
439
+ * The structure is: fields { CreatedBy { value (Link with fragment) { fields { Name } } } }
440
+ */
441
+ function extractNestedFieldRequirements(selections) {
442
+ if (!selections) {
443
+ return undefined;
444
+ }
445
+ // Find the 'fields' ObjectSelection
446
+ let fieldsSelection;
447
+ for (let i = 0, len = selections.length; i < len; i += 1) {
448
+ const sel = selections[i];
449
+ if (sel.kind === 'Object' && sel.name === 'fields') {
450
+ fieldsSelection = sel;
451
+ break;
452
+ }
453
+ }
454
+ if (!fieldsSelection || !fieldsSelection.selections) {
455
+ return undefined;
456
+ }
457
+ let nestedFieldsMap;
458
+ // Look for ObjectSelections within fields (these are spanning fields)
459
+ const fieldSelections = fieldsSelection.selections;
460
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
461
+ const fieldSel = fieldSelections[i];
462
+ if (fieldSel.kind !== 'Object') {
463
+ continue;
464
+ }
465
+ const objSel = fieldSel;
466
+ if (!objSel.selections) {
467
+ continue;
468
+ }
469
+ // Look for the 'value' Link selection
470
+ let valueLinkSelection;
471
+ for (let j = 0, jlen = objSel.selections.length; j < jlen; j += 1) {
472
+ const sel = objSel.selections[j];
473
+ if (sel.kind === 'Link' && sel.name === 'value') {
474
+ valueLinkSelection = sel;
475
+ break;
476
+ }
477
+ }
478
+ if (!valueLinkSelection || !('fragment' in valueLinkSelection)) {
479
+ continue;
480
+ }
481
+ const fragment = valueLinkSelection.fragment;
482
+ if (!fragment || !fragment.selections) {
483
+ continue;
484
+ }
485
+ // Look for the 'fields' selection within the fragment
486
+ let nestedFieldsSelection;
487
+ for (let j = 0, jlen = fragment.selections.length; j < jlen; j += 1) {
488
+ const sel = fragment.selections[j];
489
+ if (sel.kind === 'Object' && sel.name === 'fields') {
490
+ nestedFieldsSelection = sel;
491
+ break;
492
+ }
493
+ }
494
+ if (nestedFieldsSelection && nestedFieldsSelection.selections) {
495
+ const nestedFields = new Set();
496
+ for (const nestedFieldSel of nestedFieldsSelection.selections) {
497
+ nestedFields.add(nestedFieldSel.name);
498
+ }
499
+ if (nestedFields.size > 0) {
500
+ // Lazy initialize map only if we have nested fields
501
+ if (!nestedFieldsMap) {
502
+ nestedFieldsMap = new Map();
503
+ }
504
+ nestedFieldsMap.set(fieldSel.name, nestedFields);
505
+ }
506
+ }
310
507
  }
311
- return fieldNames.size > 0 ? fieldNames : undefined;
508
+ return nestedFieldsMap;
312
509
  }
313
510
 
314
511
  const TTL_DURABLE_SEGMENT = 'TTL_DURABLE_SEGMENT';
@@ -2151,4 +2348,4 @@ function ldsRuntimeBridge() {
2151
2348
  }
2152
2349
 
2153
2350
  export { ldsRuntimeBridge as default };
2154
- // version: 1.418.0-b4def2b6ce
2351
+ // version: 1.420.0-fc658f6118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-runtime-bridge",
3
- "version": "1.418.0",
3
+ "version": "1.420.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS runtime for bridge.app.",
6
6
  "main": "dist/ldsRuntimeBridge.js",
@@ -34,18 +34,18 @@
34
34
  "release:corejar": "yarn build && ../core-build/scripts/core.js --name=lds-runtime-bridge"
35
35
  },
36
36
  "dependencies": {
37
- "@salesforce/lds-bindings": "^1.418.0",
38
- "@salesforce/lds-durable-records": "^1.418.0",
39
- "@salesforce/lds-instrumentation": "^1.418.0",
40
- "@salesforce/lds-runtime-mobile": "^1.418.0",
37
+ "@salesforce/lds-bindings": "^1.420.0",
38
+ "@salesforce/lds-durable-records": "^1.420.0",
39
+ "@salesforce/lds-instrumentation": "^1.420.0",
40
+ "@salesforce/lds-runtime-mobile": "^1.420.0",
41
41
  "@salesforce/user": "0.0.21",
42
42
  "o11y": "250.7.0"
43
43
  },
44
44
  "devDependencies": {
45
- "@salesforce/lds-network-aura": "^1.418.0",
46
- "@salesforce/lds-runtime-aura": "^1.418.0",
47
- "@salesforce/lds-store-nimbus": "^1.418.0",
48
- "@salesforce/nimbus-plugin-lds": "^1.418.0",
45
+ "@salesforce/lds-network-aura": "^1.420.0",
46
+ "@salesforce/lds-runtime-aura": "^1.420.0",
47
+ "@salesforce/lds-store-nimbus": "^1.420.0",
48
+ "@salesforce/nimbus-plugin-lds": "^1.420.0",
49
49
  "babel-plugin-dynamic-import-node": "^2.3.3"
50
50
  },
51
51
  "luvioBundlesize": [