In Locker Service (LS), they use an ObservableMembrane
with a readOnlyMembrane
within it that protects cached values. This membrane immediately rejects any writes to the protected object. If you attempt to write to an existing property, you get an error that reads Cannot assign to read only property 'x' on object, while if you attempt to write a non-existent property, you get Cannot add property x, object is not extensible. However, if the object comes from a non-cacheable Apex method, it is not protected by this readOnlyMembrane
, and you're freely allowed to set any value for any property.
In LWS, they use a WeakMap
that pairs together "red values" and "blue values." I did a deep dive for this answer because of a bug in this mechanic once before. All you really need to know for this answer is that red values are "safe" and blue values are "unsafe." When you get a response from Apex, your copy of that data is a blue value, and the protected version stored in sanitized storage is a "red value."
The blue value is wrapped in a Proxy that protects the data in various ways. If the Apex method that returns the data is marked as cacheable, then any attempt to write an existing property gets the error set' on proxy: trap returned falsish for property 'x'. Attempting to write a non-existent property is allowed on the blue object. When the Apex method is not cacheable, this extra protection isn't applied, and you can write whatever you want to the blue value.
This part of the answer is mostly an educated guess from my previous experience, but I have not yet identified the exact mechanism that is causing this behavior as of yet. It appears that, for whatever reason, any writes to the response object, which is a blue value, do not end up getting copied to the red value. When this blue value is subsequently traded for a red value at some point in the future, you end up losing whatever modifications were made to the previous blue value Proxy.
You can see this appear in the code I wrote to try and diagnose this problem.
getUncached() {
return new Promise((resolve, reject) => {
uncachedMethod()
.then((result) => {
this.uncached = result;
result.checked = true;
result.message = 'Other message'
console.log("uncached result json", JSON.stringify(result));
console.log("uncached result object", result);
console.log("uncached property json", JSON.stringify(this.uncached));
console.log("uncached property object", this.uncached);
resolve();
})
.catch((err) => {
this.uncachedError = err.message;
reject(err);
});
});
}
Output:
uncached result json {"message":"Other message","checked":true}
uncached result object {message: 'Hello, world'}
uncached property json {"message":"Other message","checked":true}
uncached property object {message: 'Hello, world'}
Here, JSON.stringify is easily able to read the blue value properties we set, but the variable itself triggers a red value response, and all we get back is the original response. I get similar outputs on an Apex method that is cached, assuming I do not try to modify message
, which is what my methods are returning:
@AuraEnabled(cacheable=true)
public static Map<String, Object> cachedMethod() {
return new Map<String, Object> {
'message' => 'Hello, world'
};
}
@AuraEnabled(cacheable=false)
public static Map<String, Object> uncachedMethod() {
return new Map<String, Object> {
'message' => 'Hello, world'
};
}
I am going to try and diagnose the problem to try and understand the problem better, but I feel like this is almost certainly a bug. At the very least, when they start enabling this for all customers, it will cause problems for anyone that's taken advantage of modifying uncached Apex responses in Locker Service. I will be reporting this as a potential bug this weekend.
The workaround is to copy the values before attempting to modify them, as you've discovered. Hopefully we'll be able to get this resolved before this becomes a major issue. Uncached values should behave the same in both LS and LWS.