From 691f981547308b48c72befa938ed7128302854de Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 23 Oct 2024 11:20:22 -0700 Subject: [PATCH] feat(signals): add a way to set symbol as key (#4665) * feat(signals): add a way to set symbol as key * chore: use weakset * chore: update readme * chore: address feedback * chore: update docs * chore: add optional * chore: address feedback * chore: fix missing renames * chore: fix isomorphic test --- packages/@lwc/engine-core/README.md | 6 ++ .../@lwc/engine-core/src/framework/main.ts | 1 + .../src/framework/mutation-tracker.ts | 5 +- packages/@lwc/engine-dom/src/index.ts | 1 + .../integration-karma/helpers/test-utils.js | 8 +++ .../test/signal/protocol/index.spec.js | 18 ++++++ .../test/signal/protocol/x/signal/signal.js | 4 ++ .../test/signal/reactivity/x/signal/signal.js | 4 ++ .../@lwc/shared/src/__tests__/signals.spec.ts | 60 +++++++++++++++++++ packages/@lwc/shared/src/index.ts | 1 + packages/@lwc/shared/src/signals.ts | 30 ++++++++++ packages/@lwc/signals/package.json | 3 + packages/@lwc/signals/src/index.ts | 10 ++++ .../lwc/__tests__/isomorphic-exports.spec.ts | 1 + 14 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 packages/@lwc/shared/src/__tests__/signals.spec.ts create mode 100644 packages/@lwc/shared/src/signals.ts diff --git a/packages/@lwc/engine-core/README.md b/packages/@lwc/engine-core/README.md index beabb63a48..01c339bd3d 100644 --- a/packages/@lwc/engine-core/README.md +++ b/packages/@lwc/engine-core/README.md @@ -115,3 +115,9 @@ This experimental API enables the sanitization of HTML content by external servi ### unwrap() This experimental API enables the removal of an object's observable membrane proxy wrapper. + +### setTrustedSignalSet() + +This experimental API enables the addition of a signal as a trusted signal. If the [ENABLE_EXPERIMENTAL_SIGNALS](https://github.com/salesforce/lwc/blob/master/packages/%40lwc/features/README.md#lwcfeatures) feature is enabled, any signal value change will trigger a re-render. + +If `setTrustedSignalSet` is called more than once, it will throw an error. If it is never called, then no trusted signal validation will be performed. The same `setTrustedSignalSet` API must be called on both `@lwc/engine-dom` and `@lwc/signals`. diff --git a/packages/@lwc/engine-core/src/framework/main.ts b/packages/@lwc/engine-core/src/framework/main.ts index e354789629..17664113e6 100644 --- a/packages/@lwc/engine-core/src/framework/main.ts +++ b/packages/@lwc/engine-core/src/framework/main.ts @@ -72,3 +72,4 @@ export { default as wire } from './decorators/wire'; export { readonly } from './readonly'; export { setFeatureFlag, setFeatureFlagForTest } from '@lwc/features'; +export { setTrustedSignalSet } from '@lwc/shared'; diff --git a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts index 22d63f9cf3..c4047792ae 100644 --- a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts +++ b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { isFunction, isNull, isObject } from '@lwc/shared'; +import { isFunction, isNull, isObject, isTrustedSignal } from '@lwc/shared'; import { Signal } from '@lwc/signals'; import { JobFunction, @@ -49,11 +49,12 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = { 'value' in target && 'subscribe' in target && isFunction(target.subscribe) && + isTrustedSignal(target) && // Only subscribe if a template is being rendered by the engine tro.isObserving() ) { // Subscribe the template reactive observer's notify method, which will mark the vm as dirty and schedule hydration. - subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); + subscribeToSignal(component, target as Signal, tro.notify.bind(tro)); } } diff --git a/packages/@lwc/engine-dom/src/index.ts b/packages/@lwc/engine-dom/src/index.ts index 700c8560a0..1add267202 100644 --- a/packages/@lwc/engine-dom/src/index.ts +++ b/packages/@lwc/engine-dom/src/index.ts @@ -30,6 +30,7 @@ export { isComponentConstructor, parseFragment, parseSVGFragment, + setTrustedSignalSet, swapComponent, swapStyle, swapTemplate, diff --git a/packages/@lwc/integration-karma/helpers/test-utils.js b/packages/@lwc/integration-karma/helpers/test-utils.js index b87e7a29ab..0501a291df 100644 --- a/packages/@lwc/integration-karma/helpers/test-utils.js +++ b/packages/@lwc/integration-karma/helpers/test-utils.js @@ -642,6 +642,13 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { TEMPLATE_CLASS_NAME_OBJECT_BINDING: process.env.API_VERSION >= 62, }; + const signalValidator = new WeakSet(); + lwc.setTrustedSignalSet(signalValidator); + + function addTrustedSignal(signal) { + signalValidator.add(signal); + } + return { clearRegister, extractDataIds, @@ -667,6 +674,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { expectConsoleCalls, expectConsoleCallsDev, catchUnhandledRejectionsAndErrors, + addTrustedSignal, ...apiFeatures, }; })(LWC, jasmine, beforeAll); diff --git a/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js b/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js index 22e7483848..4284cb59d9 100644 --- a/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js +++ b/packages/@lwc/integration-karma/test/signal/protocol/index.spec.js @@ -194,6 +194,24 @@ describe('signal protocol', () => { expect(subscribe).not.toHaveBeenCalled(); }); + + it('does not subscribe if the signal is not added as trusted signal', async () => { + const elm = createElement('x-child', { is: Child }); + const subscribe = jasmine.createSpy(); + // Note this follows the shape of the signal implementation + // but it's not added as a trusted signal (add using lwc.addTrustedSignal) + const signal = { + get value() { + return 'initial value'; + }, + subscribe, + }; + elm.signal = signal; + document.body.appendChild(elm); + await Promise.resolve(); + + expect(subscribe).not.toHaveBeenCalled(); + }); }); describe('ENABLE_EXPERIMENTAL_SIGNALS not set', () => { diff --git a/packages/@lwc/integration-karma/test/signal/protocol/x/signal/signal.js b/packages/@lwc/integration-karma/test/signal/protocol/x/signal/signal.js index 7132f4d9f1..a88e8a5ef3 100644 --- a/packages/@lwc/integration-karma/test/signal/protocol/x/signal/signal.js +++ b/packages/@lwc/integration-karma/test/signal/protocol/x/signal/signal.js @@ -1,11 +1,15 @@ // Note for testing purposes the signal implementation uses LWC module resolution to simplify things. // In production the signal will come from a 3rd party library. + +import { addTrustedSignal } from 'test-utils'; + export class Signal { subscribers = new Set(); removedSubscribers = []; constructor(initialValue) { this._value = initialValue; + addTrustedSignal(this); } set value(newValue) { diff --git a/packages/@lwc/integration-karma/test/signal/reactivity/x/signal/signal.js b/packages/@lwc/integration-karma/test/signal/reactivity/x/signal/signal.js index 168a9aa21c..21fd9ea483 100644 --- a/packages/@lwc/integration-karma/test/signal/reactivity/x/signal/signal.js +++ b/packages/@lwc/integration-karma/test/signal/reactivity/x/signal/signal.js @@ -1,10 +1,14 @@ // Note for testing purposes the signal implementation uses LWC module resolution to simplify things. // In production the signal will come from a 3rd party library. + +import { addTrustedSignal } from 'test-utils'; + export class Signal { subscribers = new Set(); constructor(initialValue) { this._value = initialValue; + addTrustedSignal(this); } set value(newValue) { diff --git a/packages/@lwc/shared/src/__tests__/signals.spec.ts b/packages/@lwc/shared/src/__tests__/signals.spec.ts new file mode 100644 index 0000000000..3e2b5ebf74 --- /dev/null +++ b/packages/@lwc/shared/src/__tests__/signals.spec.ts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024, 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 { vi } from 'vitest'; + +describe('signals', () => { + let setTrustedSignalSet: (signals: WeakSet) => void; + let addTrustedSignal: (signal: object) => void; + let isTrustedSignal: (target: object) => boolean; + + beforeEach(async () => { + vi.resetModules(); + const signalsModule = await import('../signals'); + setTrustedSignalSet = signalsModule.setTrustedSignalSet; + addTrustedSignal = signalsModule.addTrustedSignal; + isTrustedSignal = signalsModule.isTrustedSignal; + }); + + describe('setTrustedSignalSet', () => { + it('should throw an error if trustedSignals is already set', () => { + setTrustedSignalSet(new WeakSet()); + expect(() => setTrustedSignalSet(new WeakSet())).toThrow( + 'Trusted Signal Set is already set!' + ); + }); + }); + + describe('addTrustedSignal', () => { + it('should add a signal to the trustedSignals set', () => { + const mockWeakSet = new WeakSet(); + setTrustedSignalSet(mockWeakSet); + const signal = {}; + addTrustedSignal(signal); + expect(isTrustedSignal(signal)).toBe(true); + }); + }); + + describe('isTrustedSignal', () => { + it('should return true for a trusted signal', () => { + const mockWeakSet = new WeakSet(); + setTrustedSignalSet(mockWeakSet); + const signal = {}; + addTrustedSignal(signal); + expect(isTrustedSignal(signal)).toBe(true); + }); + + it('should return false for an untrusted signal', () => { + const mockWeakSet = new WeakSet(); + setTrustedSignalSet(mockWeakSet); + expect(isTrustedSignal({})).toBe(false); + }); + + it('should return true for all calls when trustedSignals is not set', () => { + expect(isTrustedSignal({})).toBe(true); + }); + }); +}); diff --git a/packages/@lwc/shared/src/index.ts b/packages/@lwc/shared/src/index.ts index ad6b72be9d..cbbb2d8d98 100644 --- a/packages/@lwc/shared/src/index.ts +++ b/packages/@lwc/shared/src/index.ts @@ -18,5 +18,6 @@ export * from './namespaces'; export * from './meta'; export * from './static-part-tokens'; export * from './style'; +export * from './signals'; export { assert }; diff --git a/packages/@lwc/shared/src/signals.ts b/packages/@lwc/shared/src/signals.ts new file mode 100644 index 0000000000..5bfb66b27c --- /dev/null +++ b/packages/@lwc/shared/src/signals.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024, 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 { isFalse } from './assert'; + +let trustedSignals: WeakSet; + +export function setTrustedSignalSet(signals: WeakSet) { + isFalse(trustedSignals, 'Trusted Signal Set is already set!'); + + trustedSignals = signals; +} + +export function addTrustedSignal(signal: object) { + // This should be a no-op when the trustedSignals set isn't set by runtime + trustedSignals?.add(signal); +} + +export function isTrustedSignal(target: object): boolean { + if (!trustedSignals) { + // The runtime didn't set a trustedSignals set + // this check should only be performed for runtimes that care about filtering signals to track + // our default behavior should be to track all signals + return true; + } + return trustedSignals.has(target); +} diff --git a/packages/@lwc/signals/package.json b/packages/@lwc/signals/package.json index d7cc059fda..89c329834f 100644 --- a/packages/@lwc/signals/package.json +++ b/packages/@lwc/signals/package.json @@ -40,5 +40,8 @@ ] } } + }, + "devDependencies": { + "@lwc/shared": "8.2.0" } } diff --git a/packages/@lwc/signals/src/index.ts b/packages/@lwc/signals/src/index.ts index aed0332b0a..1b7ffed106 100644 --- a/packages/@lwc/signals/src/index.ts +++ b/packages/@lwc/signals/src/index.ts @@ -4,6 +4,10 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ + +import { addTrustedSignal } from '@lwc/shared'; +export { setTrustedSignalSet } from '@lwc/shared'; + export type OnUpdate = () => void; export type Unsubscribe = () => void; @@ -13,6 +17,12 @@ export interface Signal { } export abstract class SignalBaseClass implements Signal { + constructor() { + // Add the signal to the set of trusted signals + // that rendering engine can track + addTrustedSignal(this); + } + abstract get value(): T; private subscribers: Set = new Set(); diff --git a/packages/lwc/__tests__/isomorphic-exports.spec.ts b/packages/lwc/__tests__/isomorphic-exports.spec.ts index 428b980e96..b388397cc7 100644 --- a/packages/lwc/__tests__/isomorphic-exports.spec.ts +++ b/packages/lwc/__tests__/isomorphic-exports.spec.ts @@ -24,6 +24,7 @@ describe('isomorphic package exports', () => { 'hydrateComponent', 'isNodeFromTemplate', 'rendererFactory', + 'setTrustedSignalSet', ]); });