Skip to content

Commit

Permalink
Updated example that tracks a complex object
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra committed Nov 21, 2024
1 parent 478b2bf commit b71b57d
Show file tree
Hide file tree
Showing 18 changed files with 609 additions and 81 deletions.
9 changes: 6 additions & 3 deletions examples/demo-signals/lwc/demoSignals/shopping-cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ import updateShoppingCart from "@salesforce/apex/ShoppingCartController.updateSh
*/

// Store each state change in the cart history
export const cartHistory = $signal([]);
export const cartHistory = $signal([], {track: true});
$effect(() => {
console.log('cartHistory', JSON.stringify(cartHistory.value, null, 2));
});
let isUndoing = false;

export const undoCartChange = () => {
isUndoing = true;
const lastState = cartHistory.value[cartHistory.value.length - 1];
// Remove the last state from the history
cartHistory.value = cartHistory.value.slice(0, -1);
cartHistory.value.splice(-1, 1);
if (lastState) {
updateCart(lastState);
}
Expand Down Expand Up @@ -55,7 +58,7 @@ async function updateCartOnTheServer(newCart, previousValue, mutate) {

// Store the previous value in the history
if (shouldUpdateHistory) {
cartHistory.value = [...cartHistory.value, previousValue];
cartHistory.value.push(previousValue);
}
} catch (error) {
mutate(null, error);
Expand Down
10 changes: 8 additions & 2 deletions examples/shopping-cart/lwc/checkoutButton/checkoutButton.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import TwElement from "c/twElement";
import { $computed } from "c/signals";
import { $computed, $effect } from "c/signals";
import { shoppingCart } from "c/demoSignals";

// States
import ready from "./states/ready.html";
import loading from "./states/loading.html";

export default class CheckoutButton extends TwElement {
itemData = $computed(() => (this.itemData = shoppingCart.value)).value;
itemData = shoppingCart.value;

connectedCallback() {
$effect(() => {
this.itemData = shoppingCart.value;
});
}

render() {
return this.itemData.loading ? loading : ready;
Expand Down
7 changes: 5 additions & 2 deletions force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ function $signal(value, options) {
// The Observable Membrane proxies the passed in object to track changes
// to objects and arrays, but this introduces a performance overhead.
const shouldTrack = options?.track ?? false;
const trackableState = shouldTrack ? new TrackedState(value, notifySubscribers) : new UntrackedState(value);
const _storageOption = options?.storage?.(trackableState.get()) ?? useInMemoryStorage(trackableState.get());
const trackableState = shouldTrack
? new TrackedState(value, notifySubscribers)
: new UntrackedState(value);
const _storageOption = options?.storage?.(trackableState.get()) ??
useInMemoryStorage(trackableState.get());
const subscribers = new Set();
function getter() {
const current = _getCurrentObserver();
Expand Down
118 changes: 117 additions & 1 deletion force-app/lwc/signals/observable-membrane/base-handler.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,132 @@
/* eslint-disable */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/*
* Copyright (c) 2023, Salesforce.com, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { ArrayConcat, getPrototypeOf, getOwnPropertyNames, getOwnPropertySymbols, getOwnPropertyDescriptor, isUndefined, isExtensible, hasOwnProperty, ObjectDefineProperty, preventExtensions, ArrayPush, ObjectCreate, } from './shared';
export class BaseProxyHandler {
constructor(membrane, value) {
this.originalTarget = value;
this.membrane = membrane;
}
// Shared utility methods
wrapDescriptor(descriptor) {
if (hasOwnProperty.call(descriptor, 'value')) {
descriptor.value = this.wrapValue(descriptor.value);
}
else {
const { set: originalSet, get: originalGet } = descriptor;
if (!isUndefined(originalGet)) {
descriptor.get = this.wrapGetter(originalGet);
}
if (!isUndefined(originalSet)) {
descriptor.set = this.wrapSetter(originalSet);
}
}
return descriptor;
}
copyDescriptorIntoShadowTarget(shadowTarget, key) {
const { originalTarget } = this;
// Note: a property might get defined multiple times in the shadowTarget
// but it will always be compatible with the previous descriptor
// to preserve the object invariants, which makes these lines safe.
const originalDescriptor = getOwnPropertyDescriptor(originalTarget, key);
// TODO: it should be impossible for the originalDescriptor to ever be undefined, this `if` can be removed
/* istanbul ignore else */
if (!isUndefined(originalDescriptor)) {
const wrappedDesc = this.wrapDescriptor(originalDescriptor);
ObjectDefineProperty(shadowTarget, key, wrappedDesc);
}
}
lockShadowTarget(shadowTarget) {
const { originalTarget } = this;
const targetKeys = ArrayConcat.call(getOwnPropertyNames(originalTarget), getOwnPropertySymbols(originalTarget));
targetKeys.forEach((key) => {
this.copyDescriptorIntoShadowTarget(shadowTarget, key);
});
const { membrane: { tagPropertyKey }, } = this;
if (!isUndefined(tagPropertyKey) && !hasOwnProperty.call(shadowTarget, tagPropertyKey)) {
ObjectDefineProperty(shadowTarget, tagPropertyKey, ObjectCreate(null));
}
preventExtensions(shadowTarget);
}
// Shared Traps
get(_shadowTarget, key) {
// TODO: apply() is never called
/* istanbul ignore next */
apply(shadowTarget, thisArg, argArray) {
/* No op */
}
// TODO: construct() is never called
/* istanbul ignore next */
construct(shadowTarget, argArray, newTarget) {
/* No op */
}
get(shadowTarget, key) {
const { originalTarget, membrane: { valueObserved }, } = this;
const value = originalTarget[key];
valueObserved(originalTarget, key);
return this.wrapValue(value);
}
has(shadowTarget, key) {
const { originalTarget, membrane: { tagPropertyKey, valueObserved }, } = this;
valueObserved(originalTarget, key);
// since key is never going to be undefined, and tagPropertyKey might be undefined
// we can simply compare them as the second part of the condition.
return key in originalTarget || key === tagPropertyKey;
}
ownKeys(shadowTarget) {
const { originalTarget, membrane: { tagPropertyKey }, } = this;
// if the membrane tag key exists and it is not in the original target, we add it to the keys.
const keys = isUndefined(tagPropertyKey) || hasOwnProperty.call(originalTarget, tagPropertyKey)
? []
: [tagPropertyKey];
// small perf optimization using push instead of concat to avoid creating an extra array
ArrayPush.apply(keys, getOwnPropertyNames(originalTarget));
ArrayPush.apply(keys, getOwnPropertySymbols(originalTarget));
return keys;
}
isExtensible(shadowTarget) {
const { originalTarget } = this;
// optimization to avoid attempting to lock down the shadowTarget multiple times
if (!isExtensible(shadowTarget)) {
return false; // was already locked down
}
if (!isExtensible(originalTarget)) {
this.lockShadowTarget(shadowTarget);
return false;
}
return true;
}
getPrototypeOf(shadowTarget) {
const { originalTarget } = this;
return getPrototypeOf(originalTarget);
}
getOwnPropertyDescriptor(shadowTarget, key) {
const { originalTarget, membrane: { valueObserved, tagPropertyKey }, } = this;
// keys looked up via getOwnPropertyDescriptor need to be reactive
valueObserved(originalTarget, key);
let desc = getOwnPropertyDescriptor(originalTarget, key);
if (isUndefined(desc)) {
if (key !== tagPropertyKey) {
return undefined;
}
// if the key is the membrane tag key, and is not in the original target,
// we produce a synthetic descriptor and install it on the shadow target
desc = { value: undefined, writable: false, configurable: false, enumerable: false };
ObjectDefineProperty(shadowTarget, tagPropertyKey, desc);
return desc;
}
if (desc.configurable === false) {
// updating the descriptor to non-configurable on the shadow
this.copyDescriptorIntoShadowTarget(shadowTarget, key);
}
// Note: by accessing the descriptor, the key is marked as observed
// but access to the value, setter or getter (if available) cannot observe
// mutations, just like regular methods, in which case we just do nothing.
return this.wrapDescriptor(desc);
}
}
3 changes: 3 additions & 0 deletions force-app/lwc/signals/observable-membrane/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* eslint-disable */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/*
* Copyright (c) 2023, Salesforce.com, Inc.
* All rights reserved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ function defaultValueIsObservable(value) {
const proto = getPrototypeOf(value);
return proto === ObjectDotPrototype || proto === null || getPrototypeOf(proto) === null;
}
const defaultValueObserved = () => {
const defaultValueObserved = (obj, key) => {
/* do nothing */
};
const defaultValueMutated = () => {
const defaultValueMutated = (obj, key) => {
/* do nothing */
};
function createShadowTarget(value) {
Expand All @@ -51,7 +51,7 @@ export class ObservableMembrane {
getProxy(value) {
const unwrappedValue = unwrap(value);
if (this.valueIsObservable(unwrappedValue)) {
// When trying to extract the writable version of a readonly, we return the readonly.
// When trying to extract the writable version of a readonly we return the readonly.
if (this.readOnlyObjectGraph.get(unwrappedValue) === value) {
return value;
}
Expand All @@ -66,6 +66,9 @@ export class ObservableMembrane {
}
return value;
}
unwrapProxy(p) {
return unwrap(p);
}
getReactiveHandler(value) {
let proxy = this.reactiveObjectGraph.get(value);
if (isUndefined(proxy)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const formatter = {
};
// Inspired from paulmillr/es6-shim
// https://github.com/paulmillr/es6-shim/blob/master/es6-shim.js#L176-L185
/* istanbul ignore next */
function getGlobal() {
// the only reliable means to get the global object is `Function('return this')()`
// However, this causes CSP violations in Chrome apps.
Expand Down
97 changes: 93 additions & 4 deletions force-app/lwc/signals/observable-membrane/reactive-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isArray, unwrap, isUndefined, } from './shared';
import { toString, isArray, unwrap, isExtensible, preventExtensions, ObjectDefineProperty, hasOwnProperty, isUndefined, } from './shared';
import { BaseProxyHandler } from './base-handler';
const getterMap = new WeakMap();
const setterMap = new WeakMap();
Expand Down Expand Up @@ -44,20 +44,109 @@ export class ReactiveProxyHandler extends BaseProxyHandler {
reverseSetterMap.set(set, originalSet);
return set;
}
set(_shadowTarget, key, value) {
unwrapDescriptor(descriptor) {
if (hasOwnProperty.call(descriptor, 'value')) {
// dealing with a data descriptor
descriptor.value = unwrap(descriptor.value);
}
else {
const { set, get } = descriptor;
if (!isUndefined(get)) {
descriptor.get = this.unwrapGetter(get);
}
if (!isUndefined(set)) {
descriptor.set = this.unwrapSetter(set);
}
}
return descriptor;
}
unwrapGetter(redGet) {
const reverseGetter = reverseGetterMap.get(redGet);
if (!isUndefined(reverseGetter)) {
return reverseGetter;
}
const handler = this;
const get = function () {
// invoking the red getter with the proxy of this
return unwrap(redGet.call(handler.wrapValue(this)));
};
getterMap.set(get, redGet);
reverseGetterMap.set(redGet, get);
return get;
}
unwrapSetter(redSet) {
const reverseSetter = reverseSetterMap.get(redSet);
if (!isUndefined(reverseSetter)) {
return reverseSetter;
}
const handler = this;
const set = function (v) {
// invoking the red setter with the proxy of this
redSet.call(handler.wrapValue(this), handler.wrapValue(v));
};
setterMap.set(set, redSet);
reverseSetterMap.set(redSet, set);
return set;
}
set(shadowTarget, key, value) {
const { originalTarget, membrane: { valueMutated }, } = this;
const oldValue = originalTarget[key];
if (oldValue !== value) {
originalTarget[key] = value;
valueMutated(originalTarget, key);
}
else if (key === 'length' && isArray(originalTarget)) {
// Fix for issue #236: push will add the new index, and by the time length
// is updated, the internal length is already equal to the new length value,
// fix for issue #236: push will add the new index, and by the time length
// is updated, the internal length is already equal to the new length value
// therefore, the oldValue is equal to the value. This is the forking logic
// to support this use case.
valueMutated(originalTarget, key);
}
return true;
}
deleteProperty(shadowTarget, key) {
const { originalTarget, membrane: { valueMutated }, } = this;
delete originalTarget[key];
valueMutated(originalTarget, key);
return true;
}
setPrototypeOf(shadowTarget, prototype) {
throw new Error(`Invalid setPrototypeOf invocation for reactive proxy ${toString(this.originalTarget)}. Prototype of reactive objects cannot be changed.`);
}
preventExtensions(shadowTarget) {
if (isExtensible(shadowTarget)) {
const { originalTarget } = this;
preventExtensions(originalTarget);
// if the originalTarget is a proxy itself, it might reject
// the preventExtension call, in which case we should not attempt to lock down
// the shadow target.
// TODO: It should not actually be possible to reach this `if` statement.
// If a proxy rejects extensions, then calling preventExtensions will throw an error:
// https://codepen.io/nolanlawson-the-selector/pen/QWMOjbY
/* istanbul ignore if */
if (isExtensible(originalTarget)) {
return false;
}
this.lockShadowTarget(shadowTarget);
}
return true;
}
defineProperty(shadowTarget, key, descriptor) {
const { originalTarget, membrane: { valueMutated, tagPropertyKey }, } = this;
if (key === tagPropertyKey && !hasOwnProperty.call(originalTarget, key)) {
// To avoid leaking the membrane tag property into the original target, we must
// be sure that the original target doesn't have yet.
// NOTE: we do not return false here because Object.freeze and equivalent operations
// will attempt to set the descriptor to the same value, and expect no to throw. This
// is an small compromise for the sake of not having to diff the descriptors.
return true;
}
ObjectDefineProperty(originalTarget, key, this.unwrapDescriptor(descriptor));
// intentionally testing if false since it could be undefined as well
if (descriptor.configurable === false) {
this.copyDescriptorIntoShadowTarget(shadowTarget, key);
}
valueMutated(originalTarget, key);
return true;
}
}
Loading

0 comments on commit b71b57d

Please sign in to comment.