Merge branch 'main' into svelte-custom-renderer

pull/18317/head
Paolo Ricciuti 4 months ago committed by GitHub
commit a8296352a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly unlink batches

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: settle discarded batch

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resume outro-ed branches if they were kept around

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid waterfall-warning when async resolves to same value

@ -842,7 +842,7 @@ Reassignments of module-level declarations will not cause reactive statements to
### script_unknown_attribute ### script_unknown_attribute
``` ```
Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it
``` ```
### slot_element_deprecated ### slot_element_deprecated

@ -107,7 +107,7 @@ This code will work when the component is rendered on the client (which is why t
## script_unknown_attribute ## script_unknown_attribute
> Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it > Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it
## slot_element_deprecated ## slot_element_deprecated

@ -213,7 +213,7 @@ export function VariableDeclaration(node, context) {
location ? b.literal(location) : undefined location ? b.literal(location) : undefined
); );
call = should_save ? save(call) : b.await(call); call = should_save ? save(call, true) : b.await(call);
declarations.push(b.declarator(declarator.id, call)); declarations.push(b.declarator(declarator.id, call));
} else { } else {
@ -251,7 +251,7 @@ export function VariableDeclaration(node, context) {
location ? b.literal(location) : undefined location ? b.literal(location) : undefined
); );
call = should_save ? save(call) : b.await(call); call = should_save ? save(call, true) : b.await(call);
} }
declarations.push(b.declarator(id, call)); declarations.push(b.declarator(id, call));

@ -633,7 +633,8 @@ export function has_await_expression(node) {
/** /**
* Turns `await ...` to `(await $.save(...))()` * Turns `await ...` to `(await $.save(...))()`
* @param {ESTree.Expression} expression * @param {ESTree.Expression} expression
* @param {boolean} unset
*/ */
export function save(expression) { export function save(expression, unset = false) {
return b.call(b.await(b.call('$.save', expression))); return b.call(b.await(b.call('$.save', expression, unset && b.true)));
} }

@ -804,11 +804,11 @@ export function script_context_deprecated(node) {
} }
/** /**
* Unrecognized attribute should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it * Unrecognised attribute should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it
* @param {null | NodeLike} node * @param {null | NodeLike} node
*/ */
export function script_unknown_attribute(node) { export function script_unknown_attribute(node) {
w(node, 'script_unknown_attribute', `Unrecognized attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`); w(node, 'script_unknown_attribute', `Unrecognised attribute — should be one of \`generics\`, \`lang\` or \`module\`. If this exists for a preprocessor, ensure that the preprocessor removes it\nhttps://svelte.dev/e/script_unknown_attribute`);
} }
/** /**

@ -111,6 +111,8 @@ export class BranchManager {
var offscreen = this.#offscreen.get(key); var offscreen = this.#offscreen.get(key);
if (offscreen) { if (offscreen) {
// effect could have been outro'ed before through a prior batch — resume if necessary
resume_effect(offscreen.effect);
this.#onscreen.set(key, offscreen.effect); this.#onscreen.set(key, offscreen.effect);
this.#offscreen.delete(key); this.#offscreen.delete(key);

@ -26,6 +26,7 @@ import {
set_reactivity_loss_tracker set_reactivity_loss_tracker
} from './deriveds.js'; } from './deriveds.js';
import { aborted } from './effects.js'; import { aborted } from './effects.js';
import { queue_micro_task } from '../dom/task.js';
/** /**
* @param {Blocker[]} blockers * @param {Blocker[]} blockers
@ -152,13 +153,25 @@ export function capture() {
* `await a + b` becomes `(await $.save(a))() + b` * `await a + b` becomes `(await $.save(a))() + b`
* @template T * @template T
* @param {Promise<T>} promise * @param {Promise<T>} promise
* @param {boolean} unset
* @returns {Promise<() => T>} * @returns {Promise<() => T>}
*/ */
export async function save(promise) { export async function save(promise, unset) {
var batch = current_batch;
var restore = capture(); var restore = capture();
var value = await promise; var value = await promise;
return () => { return () => {
if (unset) {
// If this is happening outside the context of an async derived,
// context will not automatically be unset
queue_micro_task(() => {
if (batch === current_batch) {
unset_context();
}
});
}
restore(); restore();
return value; return value;
}; };
@ -357,15 +370,15 @@ export function wait(blockers) {
*/ */
export function increment_pending() { export function increment_pending() {
var effect = /** @type {Effect} */ (active_effect); var effect = /** @type {Effect} */ (active_effect);
var boundary = /** @type {Boundary} */ (effect.b); var boundary = effect.b; // undefined if called outside the render tree, e.g. a standalone $effect.root
var batch = /** @type {Batch} */ (current_batch); var batch = /** @type {Batch} */ (current_batch);
var blocking = boundary.is_rendered(); var blocking = !!boundary?.is_rendered();
boundary.update_pending_count(1, batch); boundary?.update_pending_count(1, batch);
batch.increment(blocking, effect); batch.increment(blocking, effect);
return () => { return () => {
boundary.update_pending_count(-1, batch); boundary?.update_pending_count(-1, batch);
batch.decrement(blocking, effect); batch.decrement(blocking, effect);
}; };
} }

@ -393,31 +393,30 @@ export class Batch {
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
if (this.linked && this.#pending === 0) { if (this.#pending === 0 && (this.#roots.length === 0 || next_batch !== null)) {
this.#unlink(); this.#unlink();
}
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around, // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
// else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong. // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong.
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !this.linked) { if (async_mode_flag) {
this.#commit(); this.#commit();
// Rebases can activate other batches or null it out, therefore restore the new one here // Rebases can activate other batches or null it out, therefore restore the new one here
current_batch = next_batch; current_batch = next_batch;
} }
}
// Edge case: During traversal new branches might create effects that run immediately and set state, // Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch // causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches. // once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) { if (this.#roots.length > 0) {
if (next_batch === null) { if (next_batch !== null) {
next_batch = this;
this.#link();
}
const batch = next_batch; const batch = next_batch;
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r))); batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
} else {
next_batch = this;
}
} }
if (next_batch !== null) { if (next_batch !== null) {
@ -633,6 +632,7 @@ export class Batch {
this.#fork_commit_callbacks.clear(); this.#fork_commit_callbacks.clear();
this.#unlink(); this.#unlink();
this.#deferred?.resolve();
} }
/** /**
@ -643,8 +643,6 @@ export class Batch {
} }
#commit() { #commit() {
this.#unlink();
// If there are other pending batches, they now need to be 'rebased' — // If there are other pending batches, they now need to be 'rebased' —
// in other words, we re-run block/async effects with the newly // in other words, we re-run block/async effects with the newly
// committed state, unless the batch in question has a more // committed state, unless the batch in question has a more

@ -230,9 +230,7 @@ export function async_derived(fn, label, location) {
signal.f ^= ERROR_VALUE; signal.f ^= ERROR_VALUE;
} }
internal_set(signal, value); if (DEV && location !== undefined && !signal.equals(value)) {
if (DEV && location !== undefined) {
recent_async_deriveds.add(signal); recent_async_deriveds.add(signal);
setTimeout(() => { setTimeout(() => {
@ -242,6 +240,8 @@ export function async_derived(fn, label, location) {
} }
}); });
} }
internal_set(signal, value);
} }
batch.deactivate(); batch.deactivate();

@ -1,11 +0,0 @@
<script>
setTimeout(() => {
$effect.root(() => {
async function fn() {
const value = $derived(await 1);
return { get value() { return value } };
}
fn().then(r => console.log(r.value));
})
})
</script>

@ -0,0 +1,58 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [inc_count, inc_both, shift] = target.querySelectorAll('button');
inc_both.click();
await tick();
inc_count.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment count</button>
<button>increment both</button>
<button>shift</button>
0
0
<button>0</button>
`
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment count</button>
<button>increment both</button>
<button>shift</button>
1
2
<button>1</button>
`
);
const button = /** @type {HTMLButtonElement} */ (target.querySelector('button:last-child'));
button.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment count</button>
<button>increment both</button>
<button>shift</button>
2
2
<button>2</button>
`
);
}
});

@ -0,0 +1,23 @@
<script>
let count = $state(0);
let other = $state(0);
let queued = [];
function push(v) {
if (!v) return v;
return new Promise((resolve) => {
queued.push(() => resolve(v));
});
}
</script>
<button onclick={() => count++}>increment count</button>
<button onclick={() => {count++;other++}}>increment both</button>
<button onclick={() => queued.shift()?.()}>shift</button>
{await push(other)}
{#if count % 2 === 0}
{await push(count)}
<button onclick={() => other++}>{other}</button>
{/if}

@ -0,0 +1,19 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: { dev: true },
async test({ assert, target, warnings }) {
await tick();
const [button] = target.querySelectorAll('button');
button.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>1</button>');
button.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>1</button>');
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,12 @@
<script>
let count = $state(0);
let unreactive = $derived(await push(count));
function push(v) {
if (!v) return v;
return Promise.resolve(1);
}
</script>
<button onclick={() => count++}>{unreactive}</button>

@ -1,9 +1,15 @@
import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
// Test that an async derived inside an $effect.root not connected to the component tree still works // Test that an async derived inside an $effect.root not connected to the component tree still works
async test({ assert, logs }) { async test({ assert, logs }) {
await new Promise((resolve) => setTimeout(resolve, 10)); await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(logs, [1]); assert.deepEqual(logs, [1, 1]);
const [button] = document.querySelectorAll('button');
button.click();
await tick();
assert.deepEqual(logs, [1, 1, 2]);
} }
}); });

@ -0,0 +1,19 @@
<script>
let increment;
setTimeout(() => {
$effect.root(() => {
async function fn() {
let count = $state(1);
increment = () => { count++; };
const value = $derived(await count);
$effect.pre(() => console.log(value))
return { get value() { return value } };
}
fn().then(r => console.log(r.value));
})
})
</script>
<button onclick={() => increment()}>increment</button>

@ -0,0 +1,37 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
await tick();
const [increment, pop] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
pop.click();
await tick();
assert.deepEqual(logs, ['settled 2', 'settled 2']);
assert.htmlEqual(
target.innerHTML,
`
2
<button>increment</button>
<button>pop</button>
`
);
pop.click();
await tick();
assert.deepEqual(logs, ['settled 2', 'settled 2']);
assert.htmlEqual(
target.innerHTML,
`
2
<button>increment</button>
<button>pop</button>
`
);
}
});

@ -0,0 +1,22 @@
<script>
import { settled } from "svelte";
let count = $state(0);
let queued = [];
function push(v) {
if (!v) return v;
return new Promise((resolve) => {
queued.push(() => resolve(v));
});
}
</script>
{await push(count)}
<button onclick={async () => {
count++;
await settled();
console.log('settled ' + count);
}}>increment</button>
<button onclick={() => queued.pop()?.()}>pop</button>

@ -0,0 +1,14 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, logs }) {
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(logs, [1, 1]);
const [button] = document.querySelectorAll('button');
button.click();
await tick();
assert.deepEqual(logs, [1, 1, 2]);
}
});

@ -0,0 +1,15 @@
<script>
let increment;
async function fn() {
let count = $state(1);
increment = () => { count++; };
const value = $derived(await count);
$effect.pre(() => console.log(value))
return { get value() { return value } };
}
fn().then(r => console.log(r.value));
</script>
<button onclick={() => increment()}>increment</button>

@ -1,7 +1,7 @@
[ [
{ {
"code": "script_unknown_attribute", "code": "script_unknown_attribute",
"message": "Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it", "message": "Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it",
"start": { "start": {
"column": 8, "column": 8,
"line": 1 "line": 1

@ -1,7 +1,7 @@
[ [
{ {
"code": "script_unknown_attribute", "code": "script_unknown_attribute",
"message": "Unrecognized attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it", "message": "Unrecognised attribute — should be one of `generics`, `lang` or `module`. If this exists for a preprocessor, ensure that the preprocessor removes it",
"start": { "start": {
"column": 8, "column": 8,
"line": 1 "line": 1

Loading…
Cancel
Save