@salesforce/lds-runtime-mobile 1.419.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.
Files changed (3) hide show
  1. package/dist/main.js +257 -60
  2. package/package.json +16 -16
  3. package/sfdc/main.js +257 -60
package/dist/main.js CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { withRegistration, register } from '@salesforce/lds-default-luvio';
19
19
  import { setupInstrumentation, instrumentAdapter as instrumentAdapter$1, instrumentLuvio, setLdsAdaptersUiapiInstrumentation, setLdsNetworkAdapterInstrumentation } from '@salesforce/lds-instrumentation';
20
- import { HttpStatusCode as HttpStatusCode$1, setBypassDeepFreeze, StoreKeySet, serializeStructuredKey, StringKeyInMemoryStore, Reader, deepFreeze as deepFreeze$1, emitAdapterEvent, ingestShape, coerceConfig as coerceConfig$1, typeCheckConfig as typeCheckConfig$h, createResourceParams as createResourceParams$h, StoreKeyMap, buildNetworkSnapshotCachePolicy as buildNetworkSnapshotCachePolicy$f, resolveLink, createCustomAdapterEventEmitter, isFileReference, Environment, Luvio, InMemoryStore } from '@luvio/engine';
20
+ import { HttpStatusCode as HttpStatusCode$1, setBypassDeepFreeze, StoreKeySet, StringKeyInMemoryStore, Reader, serializeStructuredKey, deepFreeze as deepFreeze$1, emitAdapterEvent, ingestShape, coerceConfig as coerceConfig$1, typeCheckConfig as typeCheckConfig$h, createResourceParams as createResourceParams$h, StoreKeyMap, buildNetworkSnapshotCachePolicy as buildNetworkSnapshotCachePolicy$f, resolveLink, createCustomAdapterEventEmitter, isFileReference, Environment, Luvio, InMemoryStore } from '@luvio/engine';
21
21
  import { isSupportedEntity, configuration, getObjectInfoAdapterFactory, RECORD_ID_PREFIX, RECORD_FIELDS_KEY_JUNCTION, isStoreKeyRecordViewEntity, extractRecordIdFromStoreKey, buildRecordRepKeyFromId, keyBuilderRecord, RecordRepresentationTTL, keyBuilderQuickActionExecutionRepresentation, ingestQuickActionExecutionRepresentation, getRecordId18 as getRecordId18$1, getRecordsAdapterFactory as getRecordsAdapterFactory$1, RecordRepresentationRepresentationType, ObjectInfoRepresentationType, getObjectInfosAdapterFactory, getObjectInfoDirectoryAdapterFactory, UiApiNamespace, RecordRepresentationType, RecordRepresentationVersion } from '@salesforce/lds-adapters-uiapi';
22
22
  import allowUpdatesForNonCachedRecords from '@salesforce/gate/lmr.allowUpdatesForNonCachedRecords';
23
23
  import { getInstrumentation, idleDetector } from 'o11y/client';
@@ -541,7 +541,7 @@ const RedirectDurableSegment$1 = 'REDIRECT_KEYS';
541
541
  const MessagingDurableSegment = 'MESSAGING';
542
542
  const MessageNotifyStoreUpdateAvailable = 'notifyStoreUpdateAvailable';
543
543
 
544
- const { keys: keys$5, create: create$5, assign: assign$4, freeze: freeze$2 } = Object;
544
+ const { keys: keys$5, create: create$5, assign: assign$4, freeze: freeze$2, isFrozen: isFrozen$1 } = Object;
545
545
 
546
546
  //Durable store error instrumentation key
547
547
  const DURABLE_STORE_ERROR = 'durable-store-error';
@@ -628,6 +628,74 @@ function publishDurableStoreEntries(durableRecords, put, publishMetadata) {
628
628
  }
629
629
  return { revivedKeys, hadUnexpectedShape };
630
630
  }
631
+ /**
632
+ * Extracts field filtering configuration from the selection.
633
+ */
634
+ function extractFieldFilteringConfig(select, recordId, baseEnvironment) {
635
+ const rootRecordKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(recordId));
636
+ let topLevelFields;
637
+ let nestedFieldRequirements;
638
+ if (select &&
639
+ select.node.kind === 'Fragment' &&
640
+ 'selections' in select.node &&
641
+ select.node.selections) {
642
+ topLevelFields = extractRequestedFieldNames(select.node.selections);
643
+ nestedFieldRequirements = extractNestedFieldRequirements(select.node.selections);
644
+ }
645
+ // Merge all nested field requirements into a single set
646
+ let nestedFields;
647
+ if (nestedFieldRequirements && nestedFieldRequirements.size > 0) {
648
+ nestedFields = new Set();
649
+ for (const fieldSet of nestedFieldRequirements.values()) {
650
+ for (const field of fieldSet) {
651
+ nestedFields.add(field);
652
+ }
653
+ }
654
+ }
655
+ return { rootRecordKey, topLevelFields, nestedFields };
656
+ }
657
+ /**
658
+ * Categorizes keys into different fetch strategies based on filtering requirements.
659
+ */
660
+ function categorizeKeysForL2Fetch(keysToRevive, config, baseEnvironment, shouldFilterFields) {
661
+ const unfilteredKeys = [];
662
+ const rootKeysWithTopLevelFields = [];
663
+ const nestedKeysWithNestedFields = [];
664
+ const canFilter = config.topLevelFields !== undefined && config.topLevelFields.size > 0;
665
+ for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
666
+ const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
667
+ if (!shouldFilterFields(canonicalKey) || !canFilter) {
668
+ unfilteredKeys.push(canonicalKey);
669
+ continue;
670
+ }
671
+ const isRootRecord = canonicalKey === config.rootRecordKey;
672
+ if (isRootRecord) {
673
+ rootKeysWithTopLevelFields.push(canonicalKey);
674
+ }
675
+ else if (config.nestedFields !== undefined && config.nestedFields.size > 0) {
676
+ nestedKeysWithNestedFields.push(canonicalKey);
677
+ }
678
+ else {
679
+ unfilteredKeys.push(canonicalKey);
680
+ }
681
+ }
682
+ return { unfilteredKeys, rootKeysWithTopLevelFields, nestedKeysWithNestedFields };
683
+ }
684
+ /**
685
+ * Builds L2 fetch promises for different key categories.
686
+ */
687
+ function buildL2FetchPromises(categorizedKeys, config, durableStore) {
688
+ const promises = [
689
+ durableStore.getEntries(categorizedKeys.unfilteredKeys, DefaultDurableSegment),
690
+ ];
691
+ if (config.topLevelFields && categorizedKeys.rootKeysWithTopLevelFields.length > 0) {
692
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.rootKeysWithTopLevelFields, config.topLevelFields, DefaultDurableSegment));
693
+ }
694
+ if (config.nestedFields && categorizedKeys.nestedKeysWithNestedFields.length > 0) {
695
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.nestedKeysWithNestedFields, config.nestedFields, DefaultDurableSegment));
696
+ }
697
+ return promises;
698
+ }
631
699
  /**
632
700
  * This method returns a Promise to a snapshot that is revived from L2 cache. If
633
701
  * L2 does not have the entries necessary to fulfill the snapshot then this method
@@ -663,53 +731,41 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
663
731
  const keysToRevive = keysToReviveSet.keysAsArray();
664
732
  const start = Date.now();
665
733
  const { l2Trips } = reviveMetrics;
666
- // Extract requested fields first to determine if filtering is possible
667
- let requestedFields;
668
- if (select.node.kind === 'Fragment' && 'selections' in select.node && select.node.selections) {
669
- requestedFields = extractRequestedFieldNames(select.node.selections);
670
- }
671
- const canonicalKeys = [];
672
- const filteredCanonicalKeys = [];
673
- for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
674
- const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
675
- // Only filter if we have fields to filter and shouldFilterFields returns true
676
- if (requestedFields !== undefined &&
677
- requestedFields.size > 0 &&
678
- shouldFilterFields(canonicalKey)) {
679
- filteredCanonicalKeys.push(canonicalKey);
680
- }
681
- else {
682
- canonicalKeys.push(canonicalKey);
683
- }
684
- }
685
- const fetchPromises = [
686
- durableStore.getEntries(canonicalKeys, DefaultDurableSegment),
687
- ];
688
- if (requestedFields !== undefined &&
689
- requestedFields.size > 0 &&
690
- filteredCanonicalKeys.length > 0) {
691
- fetchPromises.push(durableStore.getEntriesWithSpecificFields(filteredCanonicalKeys, requestedFields, DefaultDurableSegment));
692
- }
693
- return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords]) => {
734
+ // Extract field filtering requirements from the selection
735
+ const fieldFilteringConfig = extractFieldFilteringConfig(select, recordId, baseEnvironment);
736
+ // Categorize keys by how they should be fetched from L2
737
+ const categorizedKeys = categorizeKeysForL2Fetch(keysToRevive, fieldFilteringConfig, baseEnvironment, shouldFilterFields);
738
+ // Build fetch promises for each category
739
+ const fetchPromises = buildL2FetchPromises(categorizedKeys, fieldFilteringConfig, durableStore);
740
+ return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords, nestedFilteredDurableRecords]) => {
741
+ const totalKeysRequested = categorizedKeys.unfilteredKeys.length +
742
+ categorizedKeys.rootKeysWithTopLevelFields.length +
743
+ categorizedKeys.nestedKeysWithNestedFields.length;
694
744
  l2Trips.push({
695
745
  duration: Date.now() - start,
696
- keysRequestedCount: canonicalKeys.length + filteredCanonicalKeys.length,
746
+ keysRequestedCount: totalKeysRequested,
697
747
  });
698
- // Process both normal and filtered records in a single pass
748
+ // Process all three categories of records
699
749
  const revivedKeys = new StoreKeySet();
700
750
  let hadUnexpectedShape = false;
701
- // Process normal records
751
+ // Process normal records (all fields)
702
752
  if (durableRecords !== undefined) {
703
753
  const normalResult = publishDurableStoreEntries(durableRecords, baseEnvironment.storePut.bind(baseEnvironment), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
704
754
  revivedKeys.merge(normalResult.revivedKeys);
705
755
  hadUnexpectedShape = hadUnexpectedShape || normalResult.hadUnexpectedShape;
706
756
  }
707
- // Process filtered records with merging
757
+ // Process filtered records (root with top-level fields) with merging
708
758
  if (filteredDurableRecords !== undefined) {
709
759
  const filteredResult = publishDurableStoreEntries(filteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
710
760
  revivedKeys.merge(filteredResult.revivedKeys);
711
761
  hadUnexpectedShape = hadUnexpectedShape || filteredResult.hadUnexpectedShape;
712
762
  }
763
+ // Process nested filtered records with merging
764
+ if (nestedFilteredDurableRecords !== undefined) {
765
+ const nestedFilteredResult = publishDurableStoreEntries(nestedFilteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
766
+ revivedKeys.merge(nestedFilteredResult.revivedKeys);
767
+ hadUnexpectedShape = hadUnexpectedShape || nestedFilteredResult.hadUnexpectedShape;
768
+ }
713
769
  // if the data coming back from DS had an unexpected shape then just
714
770
  // return the L1 snapshot
715
771
  if (hadUnexpectedShape === true) {
@@ -755,6 +811,54 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
755
811
  return { snapshot: unavailableSnapshot, metrics: reviveMetrics };
756
812
  });
757
813
  }
814
+ /**
815
+ * Filters out null fields from a fields object (null indicates field doesn't exist in L2).
816
+ * Returns a new object without null values, or the original if no filtering needed.
817
+ */
818
+ function filterNullFields(fields) {
819
+ const keys$1 = keys$5(fields);
820
+ let hasNull = false;
821
+ // Check if any nulls exist before allocating new object
822
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
823
+ if (fields[keys$1[i]] === null) {
824
+ hasNull = true;
825
+ break;
826
+ }
827
+ }
828
+ if (!hasNull) {
829
+ return fields;
830
+ }
831
+ const cleaned = {};
832
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
833
+ const key = keys$1[i];
834
+ if (fields[key] !== null) {
835
+ cleaned[key] = fields[key];
836
+ }
837
+ }
838
+ return cleaned;
839
+ }
840
+ /**
841
+ * Merges new fields into existing fields object, skipping null values.
842
+ * Creates a new object to avoid mutations.
843
+ */
844
+ function mergeFieldsObjects(existing, incoming) {
845
+ const merged = { ...existing };
846
+ const keys$1 = keys$5(incoming);
847
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
848
+ const key = keys$1[i];
849
+ // Skip null values - they indicate the field doesn't exist in L2
850
+ if (incoming[key] !== null) {
851
+ merged[key] = incoming[key];
852
+ }
853
+ }
854
+ return merged;
855
+ }
856
+ /**
857
+ * Type guard to check if value is a non-null object.
858
+ */
859
+ function isObject$1(value) {
860
+ return typeof value === 'object' && value !== null;
861
+ }
758
862
  /**
759
863
  * Creates a put function that merges filtered fields with existing L1 records instead of replacing them.
760
864
  * This is used when reviving filtered fields from L2 to preserve existing data in L1.
@@ -762,28 +866,36 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
762
866
  function createMergeFilteredPut(readEntry, storePut) {
763
867
  return (key, filteredData) => {
764
868
  const existingRecord = readEntry(key);
765
- if (existingRecord !== undefined &&
766
- existingRecord !== null &&
767
- typeof filteredData === 'object' &&
768
- filteredData !== null &&
769
- typeof existingRecord === 'object') {
770
- const filteredFields = filteredData;
771
- const existingObj = existingRecord;
772
- // Check if object is frozen (can happen after deepFreeze)
773
- // If frozen, create a shallow copy to merge fields into
774
- let targetObj = existingObj;
775
- if (Object.isFrozen(existingObj)) {
776
- targetObj = { ...existingObj };
777
- }
778
- const keys = Object.keys(filteredFields);
779
- for (let i = 0, len = keys.length; i < len; i += 1) {
780
- const fieldKey = keys[i];
781
- targetObj[fieldKey] = filteredFields[fieldKey];
869
+ // Merge with existing record if both are objects
870
+ if (isObject$1(existingRecord) && isObject$1(filteredData)) {
871
+ // Create target object (copy if frozen to avoid mutation errors)
872
+ const targetObj = isFrozen$1(existingRecord)
873
+ ? { ...existingRecord }
874
+ : existingRecord;
875
+ const keys$1 = keys$5(filteredData);
876
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
877
+ const fieldKey = keys$1[i];
878
+ const incomingValue = filteredData[fieldKey];
879
+ // Special handling for 'fields' property to merge rather than replace
880
+ if (fieldKey === 'fields' &&
881
+ isObject$1(incomingValue) &&
882
+ isObject$1(targetObj[fieldKey])) {
883
+ targetObj[fieldKey] = mergeFieldsObjects(targetObj[fieldKey], incomingValue);
884
+ }
885
+ else {
886
+ targetObj[fieldKey] = incomingValue;
887
+ }
782
888
  }
783
889
  storePut(key, targetObj);
784
890
  }
785
891
  else {
786
- // No existing record, just put the filtered data
892
+ // No existing record - clean null fields before storing
893
+ if (isObject$1(filteredData) && 'fields' in filteredData) {
894
+ const fields = filteredData.fields;
895
+ if (isObject$1(fields)) {
896
+ filteredData.fields = filterNullFields(fields);
897
+ }
898
+ }
787
899
  storePut(key, filteredData);
788
900
  }
789
901
  };
@@ -797,16 +909,101 @@ function extractRequestedFieldNames(selections) {
797
909
  return undefined;
798
910
  }
799
911
  // Find the 'fields' ObjectSelection
800
- const fieldsSelection = selections.find((sel) => sel.kind === 'Object' && sel.name === 'fields');
801
- if (!fieldsSelection || !fieldsSelection.selections) {
912
+ let fieldsSelection;
913
+ for (let i = 0, len = selections.length; i < len; i += 1) {
914
+ const sel = selections[i];
915
+ if (sel.kind === 'Object' && sel.name === 'fields') {
916
+ fieldsSelection = sel;
917
+ break;
918
+ }
919
+ }
920
+ if (!fieldsSelection ||
921
+ !fieldsSelection.selections ||
922
+ fieldsSelection.selections.length === 0) {
802
923
  return undefined;
803
924
  }
804
925
  // Extract all field names from the fields selections
926
+ const fieldSelections = fieldsSelection.selections;
805
927
  const fieldNames = new Set();
806
- for (const fieldSel of fieldsSelection.selections) {
807
- fieldNames.add(fieldSel.name);
928
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
929
+ fieldNames.add(fieldSelections[i].name);
930
+ }
931
+ return fieldNames;
932
+ }
933
+ /**
934
+ * Extracts nested field requirements for spanning fields.
935
+ * For spanning fields like Case.CreatedBy.Name, we need to extract what fields
936
+ * are requested from the nested record (User in this case).
937
+ * The structure is: fields { CreatedBy { value (Link with fragment) { fields { Name } } } }
938
+ */
939
+ function extractNestedFieldRequirements(selections) {
940
+ if (!selections) {
941
+ return undefined;
942
+ }
943
+ // Find the 'fields' ObjectSelection
944
+ let fieldsSelection;
945
+ for (let i = 0, len = selections.length; i < len; i += 1) {
946
+ const sel = selections[i];
947
+ if (sel.kind === 'Object' && sel.name === 'fields') {
948
+ fieldsSelection = sel;
949
+ break;
950
+ }
951
+ }
952
+ if (!fieldsSelection || !fieldsSelection.selections) {
953
+ return undefined;
954
+ }
955
+ let nestedFieldsMap;
956
+ // Look for ObjectSelections within fields (these are spanning fields)
957
+ const fieldSelections = fieldsSelection.selections;
958
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
959
+ const fieldSel = fieldSelections[i];
960
+ if (fieldSel.kind !== 'Object') {
961
+ continue;
962
+ }
963
+ const objSel = fieldSel;
964
+ if (!objSel.selections) {
965
+ continue;
966
+ }
967
+ // Look for the 'value' Link selection
968
+ let valueLinkSelection;
969
+ for (let j = 0, jlen = objSel.selections.length; j < jlen; j += 1) {
970
+ const sel = objSel.selections[j];
971
+ if (sel.kind === 'Link' && sel.name === 'value') {
972
+ valueLinkSelection = sel;
973
+ break;
974
+ }
975
+ }
976
+ if (!valueLinkSelection || !('fragment' in valueLinkSelection)) {
977
+ continue;
978
+ }
979
+ const fragment = valueLinkSelection.fragment;
980
+ if (!fragment || !fragment.selections) {
981
+ continue;
982
+ }
983
+ // Look for the 'fields' selection within the fragment
984
+ let nestedFieldsSelection;
985
+ for (let j = 0, jlen = fragment.selections.length; j < jlen; j += 1) {
986
+ const sel = fragment.selections[j];
987
+ if (sel.kind === 'Object' && sel.name === 'fields') {
988
+ nestedFieldsSelection = sel;
989
+ break;
990
+ }
991
+ }
992
+ if (nestedFieldsSelection && nestedFieldsSelection.selections) {
993
+ const nestedFields = new Set();
994
+ for (const nestedFieldSel of nestedFieldsSelection.selections) {
995
+ nestedFields.add(nestedFieldSel.name);
996
+ }
997
+ if (nestedFields.size > 0) {
998
+ // Lazy initialize map only if we have nested fields
999
+ if (!nestedFieldsMap) {
1000
+ nestedFieldsMap = new Map();
1001
+ }
1002
+ nestedFieldsMap.set(fieldSel.name, nestedFields);
1003
+ }
1004
+ }
808
1005
  }
809
- return fieldNames.size > 0 ? fieldNames : undefined;
1006
+ return nestedFieldsMap;
810
1007
  }
811
1008
 
812
1009
  const TTL_DURABLE_SEGMENT = 'TTL_DURABLE_SEGMENT';
@@ -58582,7 +58779,7 @@ function buildServiceDescriptor$b(luvio) {
58582
58779
  },
58583
58780
  };
58584
58781
  }
58585
- // version: 1.419.0-8232dcd0ab
58782
+ // version: 1.420.0-576a1ea316
58586
58783
 
58587
58784
  /**
58588
58785
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -58608,7 +58805,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
58608
58805
  },
58609
58806
  };
58610
58807
  }
58611
- // version: 1.419.0-8232dcd0ab
58808
+ // version: 1.420.0-576a1ea316
58612
58809
 
58613
58810
  /*!
58614
58811
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -61260,4 +61457,4 @@ register({
61260
61457
  });
61261
61458
 
61262
61459
  export { O11Y_NAMESPACE_LDS_MOBILE, getRuntime, ingest$1o as ingestDenormalizedRecordRepresentation, initializeOneStore, registerReportObserver, reportGraphqlQueryParseError };
61263
- // version: 1.419.0-ade430921b
61460
+ // version: 1.420.0-fc658f6118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/lds-runtime-mobile",
3
- "version": "1.419.0",
3
+ "version": "1.420.0",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "description": "LDS runtime for mobile/hybrid environments.",
6
6
  "main": "dist/main.js",
@@ -35,11 +35,11 @@
35
35
  "@conduit-client/service-bindings-imperative": "3.14.0",
36
36
  "@conduit-client/service-bindings-lwc": "3.14.0",
37
37
  "@conduit-client/service-provisioner": "3.14.0",
38
- "@salesforce/lds-adapters-uiapi": "^1.419.0",
39
- "@salesforce/lds-bindings": "^1.419.0",
40
- "@salesforce/lds-instrumentation": "^1.419.0",
41
- "@salesforce/lds-luvio-service": "^1.419.0",
42
- "@salesforce/lds-luvio-uiapi-records-service": "^1.419.0",
38
+ "@salesforce/lds-adapters-uiapi": "^1.420.0",
39
+ "@salesforce/lds-bindings": "^1.420.0",
40
+ "@salesforce/lds-instrumentation": "^1.420.0",
41
+ "@salesforce/lds-luvio-service": "^1.420.0",
42
+ "@salesforce/lds-luvio-uiapi-records-service": "^1.420.0",
43
43
  "@salesforce/user": "0.0.21",
44
44
  "o11y": "250.7.0",
45
45
  "o11y_schema": "256.126.0"
@@ -60,16 +60,16 @@
60
60
  "@conduit-client/service-pubsub": "3.14.0",
61
61
  "@conduit-client/service-store": "3.14.0",
62
62
  "@conduit-client/utils": "3.14.0",
63
- "@salesforce/lds-adapters-graphql": "^1.419.0",
64
- "@salesforce/lds-drafts": "^1.419.0",
65
- "@salesforce/lds-durable-records": "^1.419.0",
66
- "@salesforce/lds-network-adapter": "^1.419.0",
67
- "@salesforce/lds-network-nimbus": "^1.419.0",
68
- "@salesforce/lds-store-binary": "^1.419.0",
69
- "@salesforce/lds-store-nimbus": "^1.419.0",
70
- "@salesforce/lds-store-sql": "^1.419.0",
71
- "@salesforce/lds-utils-adapters": "^1.419.0",
72
- "@salesforce/nimbus-plugin-lds": "^1.419.0",
63
+ "@salesforce/lds-adapters-graphql": "^1.420.0",
64
+ "@salesforce/lds-drafts": "^1.420.0",
65
+ "@salesforce/lds-durable-records": "^1.420.0",
66
+ "@salesforce/lds-network-adapter": "^1.420.0",
67
+ "@salesforce/lds-network-nimbus": "^1.420.0",
68
+ "@salesforce/lds-store-binary": "^1.420.0",
69
+ "@salesforce/lds-store-nimbus": "^1.420.0",
70
+ "@salesforce/lds-store-sql": "^1.420.0",
71
+ "@salesforce/lds-utils-adapters": "^1.420.0",
72
+ "@salesforce/nimbus-plugin-lds": "^1.420.0",
73
73
  "babel-plugin-dynamic-import-node": "^2.3.3",
74
74
  "wait-for-expect": "^3.0.2"
75
75
  },
package/sfdc/main.js CHANGED
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { withRegistration, register } from 'force/ldsEngine';
19
19
  import { setupInstrumentation, instrumentAdapter as instrumentAdapter$1, instrumentLuvio, setLdsAdaptersUiapiInstrumentation, setLdsNetworkAdapterInstrumentation } from 'force/ldsInstrumentation';
20
- import { HttpStatusCode as HttpStatusCode$1, setBypassDeepFreeze, StoreKeySet, serializeStructuredKey, StringKeyInMemoryStore, Reader, deepFreeze as deepFreeze$1, emitAdapterEvent, ingestShape, coerceConfig as coerceConfig$1, typeCheckConfig as typeCheckConfig$h, createResourceParams as createResourceParams$h, StoreKeyMap, buildNetworkSnapshotCachePolicy as buildNetworkSnapshotCachePolicy$f, resolveLink, createCustomAdapterEventEmitter, isFileReference, Environment, Luvio, InMemoryStore } from 'force/luvioEngine';
20
+ import { HttpStatusCode as HttpStatusCode$1, setBypassDeepFreeze, StoreKeySet, StringKeyInMemoryStore, Reader, serializeStructuredKey, deepFreeze as deepFreeze$1, emitAdapterEvent, ingestShape, coerceConfig as coerceConfig$1, typeCheckConfig as typeCheckConfig$h, createResourceParams as createResourceParams$h, StoreKeyMap, buildNetworkSnapshotCachePolicy as buildNetworkSnapshotCachePolicy$f, resolveLink, createCustomAdapterEventEmitter, isFileReference, Environment, Luvio, InMemoryStore } from 'force/luvioEngine';
21
21
  import { isSupportedEntity, configuration, getObjectInfoAdapterFactory, RECORD_ID_PREFIX, RECORD_FIELDS_KEY_JUNCTION, isStoreKeyRecordViewEntity, extractRecordIdFromStoreKey, buildRecordRepKeyFromId, keyBuilderRecord, RecordRepresentationTTL, keyBuilderQuickActionExecutionRepresentation, ingestQuickActionExecutionRepresentation, getRecordId18 as getRecordId18$1, getRecordsAdapterFactory as getRecordsAdapterFactory$1, RecordRepresentationRepresentationType, ObjectInfoRepresentationType, getObjectInfosAdapterFactory, getObjectInfoDirectoryAdapterFactory, UiApiNamespace, RecordRepresentationType, RecordRepresentationVersion } from 'force/ldsAdaptersUiapi';
22
22
  import allowUpdatesForNonCachedRecords from '@salesforce/gate/lmr.allowUpdatesForNonCachedRecords';
23
23
  import { getInstrumentation, idleDetector } from 'o11y/client';
@@ -541,7 +541,7 @@ const RedirectDurableSegment$1 = 'REDIRECT_KEYS';
541
541
  const MessagingDurableSegment = 'MESSAGING';
542
542
  const MessageNotifyStoreUpdateAvailable = 'notifyStoreUpdateAvailable';
543
543
 
544
- const { keys: keys$5, create: create$5, assign: assign$4, freeze: freeze$2 } = Object;
544
+ const { keys: keys$5, create: create$5, assign: assign$4, freeze: freeze$2, isFrozen: isFrozen$1 } = Object;
545
545
 
546
546
  //Durable store error instrumentation key
547
547
  const DURABLE_STORE_ERROR = 'durable-store-error';
@@ -628,6 +628,74 @@ function publishDurableStoreEntries(durableRecords, put, publishMetadata) {
628
628
  }
629
629
  return { revivedKeys, hadUnexpectedShape };
630
630
  }
631
+ /**
632
+ * Extracts field filtering configuration from the selection.
633
+ */
634
+ function extractFieldFilteringConfig(select, recordId, baseEnvironment) {
635
+ const rootRecordKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(recordId));
636
+ let topLevelFields;
637
+ let nestedFieldRequirements;
638
+ if (select &&
639
+ select.node.kind === 'Fragment' &&
640
+ 'selections' in select.node &&
641
+ select.node.selections) {
642
+ topLevelFields = extractRequestedFieldNames(select.node.selections);
643
+ nestedFieldRequirements = extractNestedFieldRequirements(select.node.selections);
644
+ }
645
+ // Merge all nested field requirements into a single set
646
+ let nestedFields;
647
+ if (nestedFieldRequirements && nestedFieldRequirements.size > 0) {
648
+ nestedFields = new Set();
649
+ for (const fieldSet of nestedFieldRequirements.values()) {
650
+ for (const field of fieldSet) {
651
+ nestedFields.add(field);
652
+ }
653
+ }
654
+ }
655
+ return { rootRecordKey, topLevelFields, nestedFields };
656
+ }
657
+ /**
658
+ * Categorizes keys into different fetch strategies based on filtering requirements.
659
+ */
660
+ function categorizeKeysForL2Fetch(keysToRevive, config, baseEnvironment, shouldFilterFields) {
661
+ const unfilteredKeys = [];
662
+ const rootKeysWithTopLevelFields = [];
663
+ const nestedKeysWithNestedFields = [];
664
+ const canFilter = config.topLevelFields !== undefined && config.topLevelFields.size > 0;
665
+ for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
666
+ const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
667
+ if (!shouldFilterFields(canonicalKey) || !canFilter) {
668
+ unfilteredKeys.push(canonicalKey);
669
+ continue;
670
+ }
671
+ const isRootRecord = canonicalKey === config.rootRecordKey;
672
+ if (isRootRecord) {
673
+ rootKeysWithTopLevelFields.push(canonicalKey);
674
+ }
675
+ else if (config.nestedFields !== undefined && config.nestedFields.size > 0) {
676
+ nestedKeysWithNestedFields.push(canonicalKey);
677
+ }
678
+ else {
679
+ unfilteredKeys.push(canonicalKey);
680
+ }
681
+ }
682
+ return { unfilteredKeys, rootKeysWithTopLevelFields, nestedKeysWithNestedFields };
683
+ }
684
+ /**
685
+ * Builds L2 fetch promises for different key categories.
686
+ */
687
+ function buildL2FetchPromises(categorizedKeys, config, durableStore) {
688
+ const promises = [
689
+ durableStore.getEntries(categorizedKeys.unfilteredKeys, DefaultDurableSegment),
690
+ ];
691
+ if (config.topLevelFields && categorizedKeys.rootKeysWithTopLevelFields.length > 0) {
692
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.rootKeysWithTopLevelFields, config.topLevelFields, DefaultDurableSegment));
693
+ }
694
+ if (config.nestedFields && categorizedKeys.nestedKeysWithNestedFields.length > 0) {
695
+ promises.push(durableStore.getEntriesWithSpecificFields(categorizedKeys.nestedKeysWithNestedFields, config.nestedFields, DefaultDurableSegment));
696
+ }
697
+ return promises;
698
+ }
631
699
  /**
632
700
  * This method returns a Promise to a snapshot that is revived from L2 cache. If
633
701
  * L2 does not have the entries necessary to fulfill the snapshot then this method
@@ -663,53 +731,41 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
663
731
  const keysToRevive = keysToReviveSet.keysAsArray();
664
732
  const start = Date.now();
665
733
  const { l2Trips } = reviveMetrics;
666
- // Extract requested fields first to determine if filtering is possible
667
- let requestedFields;
668
- if (select.node.kind === 'Fragment' && 'selections' in select.node && select.node.selections) {
669
- requestedFields = extractRequestedFieldNames(select.node.selections);
670
- }
671
- const canonicalKeys = [];
672
- const filteredCanonicalKeys = [];
673
- for (let i = 0, len = keysToRevive.length; i < len; i += 1) {
674
- const canonicalKey = serializeStructuredKey(baseEnvironment.storeGetCanonicalKey(keysToRevive[i]));
675
- // Only filter if we have fields to filter and shouldFilterFields returns true
676
- if (requestedFields !== undefined &&
677
- requestedFields.size > 0 &&
678
- shouldFilterFields(canonicalKey)) {
679
- filteredCanonicalKeys.push(canonicalKey);
680
- }
681
- else {
682
- canonicalKeys.push(canonicalKey);
683
- }
684
- }
685
- const fetchPromises = [
686
- durableStore.getEntries(canonicalKeys, DefaultDurableSegment),
687
- ];
688
- if (requestedFields !== undefined &&
689
- requestedFields.size > 0 &&
690
- filteredCanonicalKeys.length > 0) {
691
- fetchPromises.push(durableStore.getEntriesWithSpecificFields(filteredCanonicalKeys, requestedFields, DefaultDurableSegment));
692
- }
693
- return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords]) => {
734
+ // Extract field filtering requirements from the selection
735
+ const fieldFilteringConfig = extractFieldFilteringConfig(select, recordId, baseEnvironment);
736
+ // Categorize keys by how they should be fetched from L2
737
+ const categorizedKeys = categorizeKeysForL2Fetch(keysToRevive, fieldFilteringConfig, baseEnvironment, shouldFilterFields);
738
+ // Build fetch promises for each category
739
+ const fetchPromises = buildL2FetchPromises(categorizedKeys, fieldFilteringConfig, durableStore);
740
+ return Promise.all(fetchPromises).then(([durableRecords, filteredDurableRecords, nestedFilteredDurableRecords]) => {
741
+ const totalKeysRequested = categorizedKeys.unfilteredKeys.length +
742
+ categorizedKeys.rootKeysWithTopLevelFields.length +
743
+ categorizedKeys.nestedKeysWithNestedFields.length;
694
744
  l2Trips.push({
695
745
  duration: Date.now() - start,
696
- keysRequestedCount: canonicalKeys.length + filteredCanonicalKeys.length,
746
+ keysRequestedCount: totalKeysRequested,
697
747
  });
698
- // Process both normal and filtered records in a single pass
748
+ // Process all three categories of records
699
749
  const revivedKeys = new StoreKeySet();
700
750
  let hadUnexpectedShape = false;
701
- // Process normal records
751
+ // Process normal records (all fields)
702
752
  if (durableRecords !== undefined) {
703
753
  const normalResult = publishDurableStoreEntries(durableRecords, baseEnvironment.storePut.bind(baseEnvironment), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
704
754
  revivedKeys.merge(normalResult.revivedKeys);
705
755
  hadUnexpectedShape = hadUnexpectedShape || normalResult.hadUnexpectedShape;
706
756
  }
707
- // Process filtered records with merging
757
+ // Process filtered records (root with top-level fields) with merging
708
758
  if (filteredDurableRecords !== undefined) {
709
759
  const filteredResult = publishDurableStoreEntries(filteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
710
760
  revivedKeys.merge(filteredResult.revivedKeys);
711
761
  hadUnexpectedShape = hadUnexpectedShape || filteredResult.hadUnexpectedShape;
712
762
  }
763
+ // Process nested filtered records with merging
764
+ if (nestedFilteredDurableRecords !== undefined) {
765
+ const nestedFilteredResult = publishDurableStoreEntries(nestedFilteredDurableRecords, createMergeFilteredPut((key) => baseEnvironment.store.readEntry(key), baseEnvironment.storePut.bind(baseEnvironment)), baseEnvironment.publishStoreMetadata.bind(baseEnvironment));
766
+ revivedKeys.merge(nestedFilteredResult.revivedKeys);
767
+ hadUnexpectedShape = hadUnexpectedShape || nestedFilteredResult.hadUnexpectedShape;
768
+ }
713
769
  // if the data coming back from DS had an unexpected shape then just
714
770
  // return the L1 snapshot
715
771
  if (hadUnexpectedShape === true) {
@@ -755,6 +811,54 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
755
811
  return { snapshot: unavailableSnapshot, metrics: reviveMetrics };
756
812
  });
757
813
  }
814
+ /**
815
+ * Filters out null fields from a fields object (null indicates field doesn't exist in L2).
816
+ * Returns a new object without null values, or the original if no filtering needed.
817
+ */
818
+ function filterNullFields(fields) {
819
+ const keys$1 = keys$5(fields);
820
+ let hasNull = false;
821
+ // Check if any nulls exist before allocating new object
822
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
823
+ if (fields[keys$1[i]] === null) {
824
+ hasNull = true;
825
+ break;
826
+ }
827
+ }
828
+ if (!hasNull) {
829
+ return fields;
830
+ }
831
+ const cleaned = {};
832
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
833
+ const key = keys$1[i];
834
+ if (fields[key] !== null) {
835
+ cleaned[key] = fields[key];
836
+ }
837
+ }
838
+ return cleaned;
839
+ }
840
+ /**
841
+ * Merges new fields into existing fields object, skipping null values.
842
+ * Creates a new object to avoid mutations.
843
+ */
844
+ function mergeFieldsObjects(existing, incoming) {
845
+ const merged = { ...existing };
846
+ const keys$1 = keys$5(incoming);
847
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
848
+ const key = keys$1[i];
849
+ // Skip null values - they indicate the field doesn't exist in L2
850
+ if (incoming[key] !== null) {
851
+ merged[key] = incoming[key];
852
+ }
853
+ }
854
+ return merged;
855
+ }
856
+ /**
857
+ * Type guard to check if value is a non-null object.
858
+ */
859
+ function isObject$1(value) {
860
+ return typeof value === 'object' && value !== null;
861
+ }
758
862
  /**
759
863
  * Creates a put function that merges filtered fields with existing L1 records instead of replacing them.
760
864
  * This is used when reviving filtered fields from L2 to preserve existing data in L1.
@@ -762,28 +866,36 @@ function reviveSnapshot(baseEnvironment, durableStore, unavailableSnapshot, dura
762
866
  function createMergeFilteredPut(readEntry, storePut) {
763
867
  return (key, filteredData) => {
764
868
  const existingRecord = readEntry(key);
765
- if (existingRecord !== undefined &&
766
- existingRecord !== null &&
767
- typeof filteredData === 'object' &&
768
- filteredData !== null &&
769
- typeof existingRecord === 'object') {
770
- const filteredFields = filteredData;
771
- const existingObj = existingRecord;
772
- // Check if object is frozen (can happen after deepFreeze)
773
- // If frozen, create a shallow copy to merge fields into
774
- let targetObj = existingObj;
775
- if (Object.isFrozen(existingObj)) {
776
- targetObj = { ...existingObj };
777
- }
778
- const keys = Object.keys(filteredFields);
779
- for (let i = 0, len = keys.length; i < len; i += 1) {
780
- const fieldKey = keys[i];
781
- targetObj[fieldKey] = filteredFields[fieldKey];
869
+ // Merge with existing record if both are objects
870
+ if (isObject$1(existingRecord) && isObject$1(filteredData)) {
871
+ // Create target object (copy if frozen to avoid mutation errors)
872
+ const targetObj = isFrozen$1(existingRecord)
873
+ ? { ...existingRecord }
874
+ : existingRecord;
875
+ const keys$1 = keys$5(filteredData);
876
+ for (let i = 0, len = keys$1.length; i < len; i += 1) {
877
+ const fieldKey = keys$1[i];
878
+ const incomingValue = filteredData[fieldKey];
879
+ // Special handling for 'fields' property to merge rather than replace
880
+ if (fieldKey === 'fields' &&
881
+ isObject$1(incomingValue) &&
882
+ isObject$1(targetObj[fieldKey])) {
883
+ targetObj[fieldKey] = mergeFieldsObjects(targetObj[fieldKey], incomingValue);
884
+ }
885
+ else {
886
+ targetObj[fieldKey] = incomingValue;
887
+ }
782
888
  }
783
889
  storePut(key, targetObj);
784
890
  }
785
891
  else {
786
- // No existing record, just put the filtered data
892
+ // No existing record - clean null fields before storing
893
+ if (isObject$1(filteredData) && 'fields' in filteredData) {
894
+ const fields = filteredData.fields;
895
+ if (isObject$1(fields)) {
896
+ filteredData.fields = filterNullFields(fields);
897
+ }
898
+ }
787
899
  storePut(key, filteredData);
788
900
  }
789
901
  };
@@ -797,16 +909,101 @@ function extractRequestedFieldNames(selections) {
797
909
  return undefined;
798
910
  }
799
911
  // Find the 'fields' ObjectSelection
800
- const fieldsSelection = selections.find((sel) => sel.kind === 'Object' && sel.name === 'fields');
801
- if (!fieldsSelection || !fieldsSelection.selections) {
912
+ let fieldsSelection;
913
+ for (let i = 0, len = selections.length; i < len; i += 1) {
914
+ const sel = selections[i];
915
+ if (sel.kind === 'Object' && sel.name === 'fields') {
916
+ fieldsSelection = sel;
917
+ break;
918
+ }
919
+ }
920
+ if (!fieldsSelection ||
921
+ !fieldsSelection.selections ||
922
+ fieldsSelection.selections.length === 0) {
802
923
  return undefined;
803
924
  }
804
925
  // Extract all field names from the fields selections
926
+ const fieldSelections = fieldsSelection.selections;
805
927
  const fieldNames = new Set();
806
- for (const fieldSel of fieldsSelection.selections) {
807
- fieldNames.add(fieldSel.name);
928
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
929
+ fieldNames.add(fieldSelections[i].name);
930
+ }
931
+ return fieldNames;
932
+ }
933
+ /**
934
+ * Extracts nested field requirements for spanning fields.
935
+ * For spanning fields like Case.CreatedBy.Name, we need to extract what fields
936
+ * are requested from the nested record (User in this case).
937
+ * The structure is: fields { CreatedBy { value (Link with fragment) { fields { Name } } } }
938
+ */
939
+ function extractNestedFieldRequirements(selections) {
940
+ if (!selections) {
941
+ return undefined;
942
+ }
943
+ // Find the 'fields' ObjectSelection
944
+ let fieldsSelection;
945
+ for (let i = 0, len = selections.length; i < len; i += 1) {
946
+ const sel = selections[i];
947
+ if (sel.kind === 'Object' && sel.name === 'fields') {
948
+ fieldsSelection = sel;
949
+ break;
950
+ }
951
+ }
952
+ if (!fieldsSelection || !fieldsSelection.selections) {
953
+ return undefined;
954
+ }
955
+ let nestedFieldsMap;
956
+ // Look for ObjectSelections within fields (these are spanning fields)
957
+ const fieldSelections = fieldsSelection.selections;
958
+ for (let i = 0, len = fieldSelections.length; i < len; i += 1) {
959
+ const fieldSel = fieldSelections[i];
960
+ if (fieldSel.kind !== 'Object') {
961
+ continue;
962
+ }
963
+ const objSel = fieldSel;
964
+ if (!objSel.selections) {
965
+ continue;
966
+ }
967
+ // Look for the 'value' Link selection
968
+ let valueLinkSelection;
969
+ for (let j = 0, jlen = objSel.selections.length; j < jlen; j += 1) {
970
+ const sel = objSel.selections[j];
971
+ if (sel.kind === 'Link' && sel.name === 'value') {
972
+ valueLinkSelection = sel;
973
+ break;
974
+ }
975
+ }
976
+ if (!valueLinkSelection || !('fragment' in valueLinkSelection)) {
977
+ continue;
978
+ }
979
+ const fragment = valueLinkSelection.fragment;
980
+ if (!fragment || !fragment.selections) {
981
+ continue;
982
+ }
983
+ // Look for the 'fields' selection within the fragment
984
+ let nestedFieldsSelection;
985
+ for (let j = 0, jlen = fragment.selections.length; j < jlen; j += 1) {
986
+ const sel = fragment.selections[j];
987
+ if (sel.kind === 'Object' && sel.name === 'fields') {
988
+ nestedFieldsSelection = sel;
989
+ break;
990
+ }
991
+ }
992
+ if (nestedFieldsSelection && nestedFieldsSelection.selections) {
993
+ const nestedFields = new Set();
994
+ for (const nestedFieldSel of nestedFieldsSelection.selections) {
995
+ nestedFields.add(nestedFieldSel.name);
996
+ }
997
+ if (nestedFields.size > 0) {
998
+ // Lazy initialize map only if we have nested fields
999
+ if (!nestedFieldsMap) {
1000
+ nestedFieldsMap = new Map();
1001
+ }
1002
+ nestedFieldsMap.set(fieldSel.name, nestedFields);
1003
+ }
1004
+ }
808
1005
  }
809
- return fieldNames.size > 0 ? fieldNames : undefined;
1006
+ return nestedFieldsMap;
810
1007
  }
811
1008
 
812
1009
  const TTL_DURABLE_SEGMENT = 'TTL_DURABLE_SEGMENT';
@@ -58582,7 +58779,7 @@ function buildServiceDescriptor$b(luvio) {
58582
58779
  },
58583
58780
  };
58584
58781
  }
58585
- // version: 1.419.0-8232dcd0ab
58782
+ // version: 1.420.0-576a1ea316
58586
58783
 
58587
58784
  /**
58588
58785
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -58608,7 +58805,7 @@ function buildServiceDescriptor$a(notifyRecordUpdateAvailable, getNormalizedLuvi
58608
58805
  },
58609
58806
  };
58610
58807
  }
58611
- // version: 1.419.0-8232dcd0ab
58808
+ // version: 1.420.0-576a1ea316
58612
58809
 
58613
58810
  /*!
58614
58811
  * Copyright (c) 2022, Salesforce, Inc.,
@@ -61260,4 +61457,4 @@ register({
61260
61457
  });
61261
61458
 
61262
61459
  export { O11Y_NAMESPACE_LDS_MOBILE, getRuntime, ingest$1o as ingestDenormalizedRecordRepresentation, initializeOneStore, registerReportObserver, reportGraphqlQueryParseError };
61263
- // version: 1.419.0-ade430921b
61460
+ // version: 1.420.0-fc658f6118