Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add @lit-labs/signals package #4615

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

WIP: Add @lit-labs/signals package #4615

wants to merge 1 commit into from

Conversation

justinfagnani
Copy link
Collaborator

WIP: Still some rough edges with trying to sync watch() updates and regular updates.

Copy link

changeset-bot bot commented Apr 10, 2024

⚠️ No Changeset found

Latest commit: afacc80

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

github-actions bot commented Apr 10, 2024

📊 Tachometer Benchmark Results

Summary

nop-update

  • this-change, tip-of-tree, previous-release: unsure 🔍 -3% - +5% (-0.33ms - +0.58ms)
    this-change vs tip-of-tree

render

  • this-change: 45.37ms - 47.42ms
  • this-change, tip-of-tree, previous-release: unsure 🔍 -4% - +4% (-0.76ms - +0.66ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: unsure 🔍 -1% - +3% (-0.45ms - +0.88ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: unsure 🔍 -2% - +1% (-0.58ms - +0.44ms)
    this-change vs tip-of-tree

update

  • this-change: 482.03ms - 486.46ms
  • this-change, tip-of-tree, previous-release: unsure 🔍 -6% - +6% (-2.26ms - +2.24ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: unsure 🔍 -1% - +2% (-0.87ms - +1.20ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: unsure 🔍 -1% - +0% (-4.74ms - +0.78ms)
    this-change vs tip-of-tree

update-reflect

  • this-change: 490.51ms - 494.61ms
  • this-change, tip-of-tree, previous-release: unsure 🔍 -0% - +1% (-1.88ms - +4.68ms)
    this-change vs tip-of-tree

Results

this-change

render

VersionAvg timevs
45.37ms - 47.42ms-

update

VersionAvg timevs
482.03ms - 486.46ms-

update-reflect

VersionAvg timevs
490.51ms - 494.61ms-
this-change, tip-of-tree, previous-release

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
17.98ms - 18.87ms-unsure 🔍
-4% - +4%
-0.76ms - +0.66ms
unsure 🔍
-4% - +3%
-0.78ms - +0.51ms
tip-of-tree
tip-of-tree
17.92ms - 19.02msunsure 🔍
-4% - +4%
-0.66ms - +0.76ms
-unsure 🔍
-4% - +3%
-0.81ms - +0.64ms
previous-release
previous-release
18.09ms - 19.02msunsure 🔍
-3% - +4%
-0.51ms - +0.78ms
unsure 🔍
-3% - +4%
-0.64ms - +0.81ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
36.72ms - 39.89ms-unsure 🔍
-6% - +6%
-2.26ms - +2.24ms
unsure 🔍
-7% - +4%
-2.87ms - +1.72ms
tip-of-tree
tip-of-tree
36.72ms - 39.91msunsure 🔍
-6% - +6%
-2.24ms - +2.26ms
-unsure 🔍
-7% - +4%
-2.86ms - +1.74ms
previous-release
previous-release
37.23ms - 40.53msunsure 🔍
-5% - +8%
-1.72ms - +2.87ms
unsure 🔍
-5% - +8%
-1.74ms - +2.86ms
-

nop-update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
10.41ms - 11.09ms-unsure 🔍
-3% - +5%
-0.33ms - +0.58ms
unsure 🔍
-5% - +4%
-0.58ms - +0.39ms
tip-of-tree
tip-of-tree
10.32ms - 10.92msunsure 🔍
-5% - +3%
-0.58ms - +0.33ms
-unsure 🔍
-6% - +2%
-0.68ms - +0.24ms
previous-release
previous-release
10.49ms - 11.19msunsure 🔍
-4% - +5%
-0.39ms - +0.58ms
unsure 🔍
-2% - +6%
-0.24ms - +0.68ms
-
this-change, tip-of-tree, previous-release

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
32.90ms - 33.92ms-unsure 🔍
-1% - +3%
-0.45ms - +0.88ms
unsure 🔍
-3% - +1%
-0.97ms - +0.45ms
tip-of-tree
tip-of-tree
32.77ms - 33.62msunsure 🔍
-3% - +1%
-0.88ms - +0.45ms
-unsure 🔍
-3% - +0%
-1.12ms - +0.17ms
previous-release
previous-release
33.18ms - 34.15msunsure 🔍
-1% - +3%
-0.45ms - +0.97ms
unsure 🔍
-1% - +3%
-0.17ms - +1.12ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
67.94ms - 69.31ms-unsure 🔍
-1% - +2%
-0.87ms - +1.20ms
unsure 🔍
-3% - +0%
-2.02ms - +0.36ms
tip-of-tree
tip-of-tree
67.69ms - 69.23msunsure 🔍
-2% - +1%
-1.20ms - +0.87ms
-unsure 🔍
-3% - +0%
-2.24ms - +0.24ms
previous-release
previous-release
68.49ms - 70.43msunsure 🔍
-1% - +3%
-0.36ms - +2.02ms
unsure 🔍
-0% - +3%
-0.24ms - +2.24ms
-
this-change, tip-of-tree, previous-release

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
30.10ms - 30.81ms-unsure 🔍
-2% - +1%
-0.58ms - +0.44ms
unsure 🔍
-2% - +1%
-0.64ms - +0.42ms
tip-of-tree
tip-of-tree
30.16ms - 30.89msunsure 🔍
-1% - +2%
-0.44ms - +0.58ms
-unsure 🔍
-2% - +2%
-0.57ms - +0.50ms
previous-release
previous-release
30.17ms - 30.95msunsure 🔍
-1% - +2%
-0.42ms - +0.64ms
unsure 🔍
-2% - +2%
-0.50ms - +0.57ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
473.34ms - 476.74ms-unsure 🔍
-1% - +0%
-4.74ms - +0.78ms
unsure 🔍
-1% - +0%
-3.27ms - +1.77ms
tip-of-tree
tip-of-tree
474.84ms - 479.20msunsure 🔍
-0% - +1%
-0.78ms - +4.74ms
-unsure 🔍
-0% - +1%
-1.64ms - +4.10ms
previous-release
previous-release
473.93ms - 477.65msunsure 🔍
-0% - +1%
-1.77ms - +3.27ms
unsure 🔍
-1% - +0%
-4.10ms - +1.64ms
-

update-reflect

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
515.38ms - 519.93ms-unsure 🔍
-0% - +1%
-1.88ms - +4.68ms
unsure 🔍
-1% - +1%
-3.74ms - +3.70ms
tip-of-tree
tip-of-tree
513.89ms - 518.62msunsure 🔍
-1% - +0%
-4.68ms - +1.88ms
-unsure 🔍
-1% - +0%
-5.19ms - +2.36ms
previous-release
previous-release
514.73ms - 520.61msunsure 🔍
-1% - +1%
-3.70ms - +3.74ms
unsure 🔍
-0% - +1%
-2.36ms - +5.19ms
-

tachometer-reporter-action v2 for Benchmarks

Copy link
Contributor

The size of lit-html.js and lit-core.min.js are as expected.

Copy link
Member

@sorvell sorvell left a comment

Choose a reason for hiding this comment

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

For watch, let's consider @e111077 's feedback and maybe have a way to auto-wrap a function (maybe if it's not an event part?).

And I really like using the html and not manually watching but if we do that we probably want unwatched which is just basically an echoing directive.

@@ -0,0 +1,127 @@
# @lit-labs/preact-signals

Preact Signals integration for Lit.
Copy link
Member

Choose a reason for hiding this comment

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

Not preact

Copy link
Member

Choose a reason for hiding this comment

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

+ the "why preact signals" section. Perhaps even worth updating the preact-signals labs package readme to point to this one


render() {
return html`
<p>The count is ${count.value}</p>
Copy link
Member

Choose a reason for hiding this comment

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

count.get()

}

private _onClick() {
count.value = count.value + 1;
Copy link
Member

Choose a reason for hiding this comment

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

count.set(count.get() + 1)

}

private _onClick() {
count.value = count.value + 1;
Copy link
Member

Choose a reason for hiding this comment

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

count.set(count.get() + 1)

}

private _onClick() {
count.value = count.value + 1;
Copy link
Member

Choose a reason for hiding this comment

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

count.set(count.get() + 1)


override connectedCallback(): void {
if (!this.__isWatching) {
this.__isWatching = true;
Copy link
Member

Choose a reason for hiding this comment

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

Why is the flag needed? when would this happen when __isWatching is true? And I believe you can use Signal.subtle.hasSources(this.__watcher) if the state is needed (although an explicit flag in that case might be simpler).

private __forceUpdateSignal = new Signal.State(0);
private __updateSignal = new Signal.Computed(() => {
this.__forceUpdateSignal.get();
super.performUpdate();
Copy link
Member

Choose a reason for hiding this comment

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

I think you have to call this.__watcher.watch() here to make sure to re-activate the watcher. Make sure to avoid passing the signal due to tc39/proposal-signals#157

): void {
this.__shouldRender = true;
if (!this.__watcherUpdate) {
this.__forceUpdateSignal?.set(this.__forceUpdateSignal.get() + 1);
Copy link
Member

Choose a reason for hiding this comment

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

Is the idea that this ensures the update occurs even if performUpdate didn't get any signals? (e.g. you didn't actually use any signals)? If so, let's comment that.

this.__shouldRender = false;
super.update(changedProperties);
} else {
this.__pendingWatches.forEach((d) => d.commmit());
Copy link
Member

Choose a reason for hiding this comment

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

Calling commit here couples this pretty tightly with the directive. This is why I just passed a callback. No strong opinion though.

});

commmit() {
this.setValue(Signal.subtle.untrack(() => this.__signal?.get()));
Copy link
Member

Choose a reason for hiding this comment

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

Again here, I think you have to call this.__watcher.watch() to reset the watcher.

@@ -0,0 +1,127 @@
# @lit-labs/preact-signals

Preact Signals integration for Lit.
Copy link
Member

Choose a reason for hiding this comment

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

+ the "why preact signals" section. Perhaps even worth updating the preact-signals labs package readme to point to this one

export const defaultConfig = (options = {}) =>
litProdConfig({
packageName: createRequire(import.meta.url)('./package.json').name,
entryPoints: ['index', 'lib/signal-watcher', 'lib/watch'],
Copy link
Member

Choose a reason for hiding this comment

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

Why do we separate watch and signal-watcher but not the html tag module?

@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2018 Google LLC
Copy link
Member

Choose a reason for hiding this comment

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

Consider updating copyright years

// one is available. See https://github.com/preactjs/signals/issues/402
return coreTag(
strings,
...values.map((v) => (v instanceof Signal.State || v instanceof Signal.Computed? watch(v) : v))
Copy link
Member

Choose a reason for hiding this comment

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

How would we recommend handling ternaries that use a signal here or a signal.get().map(...)? I think this answer is different for SignalWatcher() and non-SignalWatcher() elements. SignalWatcher() elements using this html tag for convenience should just .get() the value and it works:

class MyElement extends SignalWatcher(LitElement) {
...
html`Clicked ${count} time${count.get() !== 1 ? 's' : nothing}.`;
...

For non-SignalWatcher() elements we need something like computed or for watch() to handle watchable functions natively:

class MyElement extends LitElement {
...
html`Clicked ${count} time${computed(() => (count.get() !== 1 ? 's' : nothing))}.`;

// some sort of function that is marked as watchable because we do not
// want to arbitrarily call bound functions like
// html`<my-textfield .checkValidity=${(value) => {...}}>`
html`Clicked ${count} time${() => (count.get() !== 1 ? 's' : nothing)}.`;
...

Also i think mentioned elsewhere in the PR but would be cool to opt-out of watch() in some cases to pass the signal reference directly.

Base: T
): T {
abstract class SignalWatcher extends Base {
// Watcher.watch() doesn't dedupe so we need to track this ourselves.
Copy link
Member

Choose a reason for hiding this comment

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

Link to bug in comment

tc39/proposal-signals#157

export const Computed = Signal.Computed;
export const subtle = Signal.subtle;

export const signal = <T>(value: T) => new Signal.State(value);
Copy link
Member

Choose a reason for hiding this comment

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

optional options arg for equality fn is a must


override disconnectedCallback(): void {
this.__isWatching = false;
this.__watcher.unwatch(this.__updateSignal);
Copy link
Member

Choose a reason for hiding this comment

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

what do we think about allowing users to unwatch the component? For example, in an MPA people might not want a component that's connected but not visible to re-render due to signal changes. Probably punt this until it's a real problem

// @ts-expect-error: signal is unused, but the name appears in the signature
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render(signal: Signal.State<T> | Signal.Computed<T>): T {
return undefined as T;
Copy link
Member

Choose a reason for hiding this comment

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

SSR sad, is there really no good solution here with the current proposal, even with an unwatch?

if (this.__signal !== undefined) {
this.__watcher.unwatch(this.__signal);
}
if (signal !== undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

When is this false? If it's ever false won't line 49 throw?

@JosefJezek
Copy link

@justinfagnani any progress?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants