Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/src/framework/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function renderComponent(vm: VM): VNodes {
}

vm.tro.reset();
vm.signalsToUnsubscribe.forEach((cb) => cb());
const vnodes = invokeComponentRenderMethod(vm);
vm.isDirty = false;
vm.isScheduled = false;
Expand Down
5 changes: 3 additions & 2 deletions packages/@lwc/engine-core/src/framework/decorators/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export function createPublicPropertyDescriptor(key: string): PropertyDescriptor
}
return;
}
componentValueObserved(vm, key);
return vm.cmpProps[key];
const val = vm.cmpProps[key];
componentValueObserved(vm, key, val);
return val;
},
set(this: LightningElement, newValue: any) {
const vm = getAssociatedVM(this);
Expand Down
5 changes: 3 additions & 2 deletions packages/@lwc/engine-core/src/framework/decorators/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export function internalTrackDecorator(key: string): PropertyDescriptor {
return {
get(this: LightningElement): any {
const vm = getAssociatedVM(this);
componentValueObserved(vm, key);
return vm.cmpFields[key];
const val = vm.cmpFields[key];
componentValueObserved(vm, key, val);
return val;
},
set(this: LightningElement, newValue: any) {
const vm = getAssociatedVM(this);
Expand Down
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 @@ -42,6 +42,7 @@ export { freezeTemplate } from './freeze-template';

// Experimental or Internal APIs
export { getComponentConstructor } from './get-component-constructor';
export { Signal, SignalBaseClass } from '../libs/signal';

// Types -------------------------------------------------------------------------------------------
export type { RendererAPI, LifecycleCallback } from './renderer';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ReactiveObserver,
valueMutated,
valueObserved,
signalValueObserved,
} from '../libs/mutation-tracker';
import { VM } from './vm';

Expand All @@ -28,11 +29,15 @@ export function componentValueMutated(vm: VM, key: PropertyKey) {
}
}

export function componentValueObserved(vm: VM, key: PropertyKey) {
export function componentValueObserved(vm: VM, key: PropertyKey, target: any = {}) {
// On the server side, we don't need mutation tracking. Skipping it improves performance.
if (process.env.IS_BROWSER) {
valueObserved(vm.component, key);
}

if (target && typeof target === 'object' && 'value' in target && 'subscribe' in target) {
signalValueObserved(target, vm.signalUpdateCallback, vm.signalsToUnsubscribe);
}
}

export function createReactiveObserver(callback: CallbackFunction): ReactiveObserver {
Expand Down
5 changes: 3 additions & 2 deletions packages/@lwc/engine-core/src/framework/observed-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export function createObservedFieldPropertyDescriptor(key: string): PropertyDesc
return {
get(this: LightningElement): any {
const vm = getAssociatedVM(this);
componentValueObserved(vm, key);
return vm.cmpFields[key];
const val = vm.cmpFields[key];
componentValueObserved(vm, key, val);
return val;
},
set(this: LightningElement, newValue: any) {
const vm = getAssociatedVM(this);
Expand Down
13 changes: 13 additions & 0 deletions packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ export interface VM<N = HostNode, E = HostElement> {
* API version associated with this VM
*/
apiVersion: APIVersion;
// tracks signals to unsubscribe from for a given vm
signalsToUnsubscribe: Array<() => void>;

signalUpdateCallback: () => void;
}

type VMAssociable = HostNode | LightningElement;
Expand Down Expand Up @@ -275,6 +279,7 @@ function resetComponentStateWhenRemoved(vm: VM) {
const { tro } = vm;
// Making sure that any observing record will not trigger the rehydrated on this vm
tro.reset();
vm.signalsToUnsubscribe.forEach((cb) => cb());
runDisconnectedCallback(vm);
// Spec: https://dom.spec.whatwg.org/#concept-node-remove (step 14-15)
runChildNodesDisconnectedCallback(vm);
Expand Down Expand Up @@ -367,6 +372,14 @@ export function createVM<HostNode, HostElement>(
shadowMode: null!,
shadowMigrateMode: false,
stylesheets: null!,
signalsToUnsubscribe: [],
signalUpdateCallback: () => {
if (isFalse(vm.isDirty)) {
// forcing the vm to rehydrate in the next tick
markComponentAsDirty(vm);
scheduleRehydration(vm);
}
},

// Properties set by the LightningElement constructor.
component: null!,
Expand Down
29 changes: 29 additions & 0 deletions packages/@lwc/engine-core/src/libs/mutation-tracker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { create, isUndefined, ArraySplice, ArrayIndexOf, ArrayPush } from '@lwc/shared';
import { OnUpdate, Signal, Unsubscribe } from '../signal';

const TargetToReactiveRecordMap: WeakMap<object, ReactiveRecord> = new WeakMap();

Expand Down Expand Up @@ -64,6 +65,34 @@ export function valueObserved(target: object, key: PropertyKey) {
}
}

// Putting this here for now, the idea is to subscribe to a signal when there is an active template reactive observer.
// This would indicate that:
// 1. The template is currently being rendered
// 2. There was a call to a getter bound to the LWC class
// With this information we can infer that it is safe to subscribe the re-render callback to the signal, which will
// mark the VM as dirty and schedule rehydration.
// The onUpdate function here mirrors the one used in the template reactive observer, which is to mark the
// VM as dirty and re-render.
// In a future optimization, rather than re-render the entire VM we could use fine grained reactivity here
// to only re-render the part of the DOM that has been changed by the signal.
// jtu-todo: find a better place to put this that doesn't require mutation-tracker to know about the specifics of signals
export function signalValueObserved(
s: Signal<any>,
onUpdate: OnUpdate,
unsubscribe: Array<Unsubscribe>
) {
if (currentReactiveObserver === null) {
return;
}

try {
const dispose = s.subscribe(onUpdate);
unsubscribe.push(dispose);
} catch (e) {
// throw away for now
}
}

export type CallbackFunction = (rp: ReactiveObserver) => void;
export type JobFunction = () => void;

Expand Down
33 changes: 33 additions & 0 deletions packages/@lwc/engine-core/src/libs/signal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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
*/

export type OnUpdate = () => void;
export type Unsubscribe = () => void;

export interface Signal<ValueShape> {
get value(): ValueShape;
subscribe(onUpdate: OnUpdate): Unsubscribe;
}

export abstract class SignalBaseClass<ValueShape> implements Signal<ValueShape> {
abstract get value(): ValueShape;

private subscribers: Set<OnUpdate> = new Set();

subscribe(onUpdate: OnUpdate) {
this.subscribers.add(onUpdate);
return () => {
this.subscribers.delete(onUpdate);
};
}

protected notify() {
for (const subscriber of this.subscribers) {
subscriber();
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed, I'd love for this to be moved to a new top-level @lwc/signals package that exposes the types and base class. I would have recommended moving to @lwc/shared, except that is intended as an internal package with other @lwc/* packages being the only direct consumers. In contrast, the signals type and base call may be re-used across the SF ecosystem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@divmain I moved everything to a new package and added a readme.

If you get a chance, can you look through the readme too?

2 changes: 2 additions & 0 deletions packages/@lwc/engine-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export {
getComponentConstructor,
__unstable__ProfilerControl,
__unstable__ReportingControl,
Signal,
SignalBaseClass,
} from '@lwc/engine-core';

// Engine-dom public APIs --------------------------------------------------------------------------
Expand Down