Skip to content

fix: address runtime effect issues #9417

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

Merged
merged 7 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/khaki-mails-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: tighten up signals implementation
1 change: 0 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ Please note that [the Svelte codebase is currently being rewritten for Svelte 5]

If your PR concerns Svelte 4 (including updates to [svelte.dev.docs](https://svelte.dev/docs)), please ensure the base branch is `svelte-4` and not `main`.


### Before submitting the PR, please make sure you do the following

- [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ export const function_visitor = (node, context) => {
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';

state = { ...context.state, in_constructor };
} else {
state = { ...context.state, in_constructor: false };
}

if (metadata?.hoistable === true) {
Expand Down
19 changes: 8 additions & 11 deletions packages/svelte/src/internal/client/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@ const has_browser_globals = typeof window !== 'undefined';
// than megamorphic.
const node_prototype = /** @type {Node} */ (has_browser_globals ? Node.prototype : {});
const element_prototype = /** @type {Element} */ (has_browser_globals ? Element.prototype : {});
const event_target_prototype = /** @type {EventTarget} */ (
has_browser_globals ? EventTarget.prototype : {}
);
const text_prototype = /** @type {Text} */ (has_browser_globals ? Text.prototype : {});
const map_prototype = Map.prototype;
const append_child_method = node_prototype.appendChild;
const clone_node_method = node_prototype.cloneNode;
const map_set_method = map_prototype.set;
const map_get_method = map_prototype.get;
const map_delete_method = map_prototype.delete;
// @ts-expect-error improve perf of expando on DOM nodes for events
event_target_prototype.__click = undefined;
// @ts-expect-error improve perf of expando on DOM textValue updates
event_target_prototype.__nodeValue = ' ';
// @ts-expect-error improve perf of expando on DOM events
element_prototype.__click = undefined;
// @ts-expect-error improve perf of expando on DOM text updates
text_prototype.__nodeValue = ' ';
// @ts-expect-error improve perf of expando on DOM className updates
event_target_prototype.__className = '';
element_prototype.__className = '';

const first_child_get = /** @type {(this: Node) => ChildNode | null} */ (
// @ts-ignore
Expand Down Expand Up @@ -162,11 +160,10 @@ export function set_class_name(node, class_name) {
/**
* @template {Node} N
* @param {N} node
* @param {string} text
* @returns {void}
*/
export function text_content(node, text) {
text_content_set.call(node, text);
export function clear_text_content(node) {
text_content_set.call(node, '');
}

/** @param {string} name */
Expand Down
6 changes: 3 additions & 3 deletions packages/svelte/src/internal/client/reconciler.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { append_child, map_get, map_set, text_content } from './operations.js';
import { append_child, map_get, map_set, clear_text_content } from './operations.js';
import {
current_hydration_fragment,
get_hydration_fragment,
Expand Down Expand Up @@ -198,7 +198,7 @@ export function reconcile_indexed_array(
b_blocks = [];
// Remove old blocks
if (is_controlled && a !== 0) {
text_content(dom, '');
clear_text_content(dom);
}
while (index < length) {
block = a_blocks[index++];
Expand Down Expand Up @@ -295,7 +295,7 @@ export function reconcile_tracked_array(
b_blocks = [];
// Remove old blocks
if (is_controlled && a !== 0) {
text_content(dom, '');
clear_text_content(dom);
}
while (a > 0) {
block = a_blocks[--a];
Expand Down
8 changes: 4 additions & 4 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -1270,15 +1270,15 @@ function handle_event_propagation(root_element, event) {
}

// composedPath contains list of nodes the event has propagated through.
// We check __handled_event_at to skip all nodes below it in case this is a
// parent of the __handled_event_at node, which indicates that there's nested
// We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested
// mounted apps. In this case we don't want to trigger events multiple times.
// We're deliberately not skipping if the index is the same or higher, because
// someone could create an event programmatically and emit it multiple times,
// in which case we want to handle the whole propagation chain properly each time.
let path_idx = 0;
// @ts-expect-error is added below
const handled_at = event.__handled_event_at;
const handled_at = event.__root;
if (handled_at) {
const at_idx = path.indexOf(handled_at);
if (at_idx < path.indexOf(root_element)) {
Expand Down Expand Up @@ -1317,7 +1317,7 @@ function handle_event_propagation(root_element, event) {
}

// @ts-expect-error is used above
event.__handled_event_at = root_element;
event.__root = root_element;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,13 @@ function destroy_references(signal) {
if (references !== null) {
let i;
for (i = 0; i < references.length; i++) {
destroy_signal(references[i]);
const reference = references[i];
if ((reference.flags & IS_EFFECT) !== 0) {
destroy_signal(reference);
} else {
remove_consumer(reference, 0, true);
reference.dependencies = null;
}
}
}
}
Expand Down Expand Up @@ -710,7 +716,7 @@ export function exposable(fn) {
export function get(signal) {
const flags = signal.flags;
if ((flags & DESTROYED) !== 0) {
return /** @type {V} */ (UNINITIALIZED);
return signal.value;
}

if (is_signal_exposed && current_should_capture_signal) {
Expand Down Expand Up @@ -1156,6 +1162,11 @@ export function managed_pre_effect(init, sync) {
* @returns {import('./types.js').EffectSignal}
*/
export function pre_effect(init) {
if (current_effect === null) {
throw new Error(
'The Svelte $effect.pre rune can only be used during component initialisation.'
);
}
const sync = current_effect !== null && (current_effect.flags & RENDER_EFFECT) !== 0;
return internal_create_effect(
PRE_EFFECT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
html: `<button>10</button>`,
ssrHtml: `<button>0</button>`,

async test({ assert, target }) {
flushSync();

assert.htmlEqual(target.innerHTML, `<button>10</button>`);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
class Counter {
count = $state(0);

constructor() {
$effect(() => {
this.count = 10;
});
}
}
const counter = new Counter();
</script>

<button on:click={() => counter.count++}>{counter.count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
html: `<button>10</button>`,
ssrHtml: `<button>0</button>`,

async test({ assert, target }) {
flushSync();

assert.htmlEqual(target.innerHTML, `<button>10</button>`);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script>
class Counter {
#count = $state(0);

constructor() {
$effect(() => {
this.#count = 10;
});
}

getCount() {
return this.#count;
}

increment() {
this.#count++;
}
}
const counter = new Counter();
</script>

<button on:click={() => counter.increment()}>{counter.getCount()}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test } from '../../test';
import { flushSync } from 'svelte';

export default test({
get props() {
return { log: [] };
},

async test({ assert, target, component }) {
const [b1] = target.querySelectorAll('button');
flushSync(() => {
b1.click();
});
flushSync(() => {
b1.click();
});
assert.deepEqual(component.log, ['init 0', 'cleanup 2', 'init 2', 'cleanup 4', 'init 4']);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
const {log} = $props();

let count = $state(0);

$effect(() => {
let double = $derived(count * 2)

log.push('init ' + double);
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 this test can move into tests/signals - or do we need something Svelte-render-specific here? Seems purely related to the signals runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It matches the other tests inside the directory that checks logging order? It seems related to Svelte runtime too.

Copy link
Member

Choose a reason for hiding this comment

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

Which other tests are you talking about? Maybe those other tests need to move into tests/signals, too 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried, but I now get test failures accessing component. Let's leave them for now and address in another PR.

Copy link
Member

Choose a reason for hiding this comment

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

what test failures about accessing component? When you try to turn this test into a test in signals then it fails or what do you mean exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test suite setup is different I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I've done some digging and ultimately I don't think these are signals tests and they shouldn't be coupled with being so. Signal tests should be agnostic of Svelte's components. However, these cases come from bug reports where users have created components that demonstrate ordering issues or timing issues that are relevant to their components. This is important as the signals tests also demonstrate unowned signals – which mean they are independent of components too, not to mention how this all fits into equality.

Let's merge this PR and create a separate issue on how we can unify the semantics around signals - vs - component state semantics, as I think there needs to be a distinction and having one is important. For example, our current signals tests are all wrapped with a render effect, which is likely wrong too. They shouldn't be, signals should be agnostic of effects.

Copy link
Member

Choose a reason for hiding this comment

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

The idea of wrapping them is to simulate this being run inside a component context, which is why I was so confused why it wouldn't work when having them in that test suite


return () => {
log.push('cleanup ' + double);
};
})
</script>

<button on:click={() => count++ }>Click</button>