Skip to content

Commit

Permalink
feat(signals): add a way to set symbol as key (#4665)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rax-it authored Oct 23, 2024
1 parent 2c57ef0 commit 691f981
Show file tree
Hide file tree
Showing 14 changed files with 150 additions and 2 deletions.
6 changes: 6 additions & 0 deletions packages/@lwc/engine-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 3 additions & 2 deletions packages/@lwc/engine-core/src/framework/mutation-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<any>, tro.notify.bind(tro));
subscribeToSignal(component, target as Signal<unknown>, tro.notify.bind(tro));
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export {
isComponentConstructor,
parseFragment,
parseSVGFragment,
setTrustedSignalSet,
swapComponent,
swapStyle,
swapTemplate,
Expand Down
8 changes: 8 additions & 0 deletions packages/@lwc/integration-karma/helpers/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -667,6 +674,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
expectConsoleCalls,
expectConsoleCallsDev,
catchUnhandledRejectionsAndErrors,
addTrustedSignal,
...apiFeatures,
};
})(LWC, jasmine, beforeAll);
18 changes: 18 additions & 0 deletions packages/@lwc/integration-karma/test/signal/protocol/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
60 changes: 60 additions & 0 deletions packages/@lwc/shared/src/__tests__/signals.spec.ts
Original file line number Diff line number Diff line change
@@ -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<object>) => 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);
});
});
});
1 change: 1 addition & 0 deletions packages/@lwc/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ export * from './namespaces';
export * from './meta';
export * from './static-part-tokens';
export * from './style';
export * from './signals';

export { assert };
30 changes: 30 additions & 0 deletions packages/@lwc/shared/src/signals.ts
Original file line number Diff line number Diff line change
@@ -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<object>;

export function setTrustedSignalSet(signals: WeakSet<object>) {
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);
}
3 changes: 3 additions & 0 deletions packages/@lwc/signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@
]
}
}
},
"devDependencies": {
"@lwc/shared": "8.2.0"
}
}
10 changes: 10 additions & 0 deletions packages/@lwc/signals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -13,6 +17,12 @@ export interface Signal<T> {
}

export abstract class SignalBaseClass<T> implements Signal<T> {
constructor() {
// Add the signal to the set of trusted signals
// that rendering engine can track
addTrustedSignal(this);
}

abstract get value(): T;

private subscribers: Set<OnUpdate> = new Set();
Expand Down
1 change: 1 addition & 0 deletions packages/lwc/__tests__/isomorphic-exports.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('isomorphic package exports', () => {
'hydrateComponent',
'isNodeFromTemplate',
'rendererFactory',
'setTrustedSignalSet',
]);
});

Expand Down

0 comments on commit 691f981

Please sign in to comment.