Merge remote-tracking branch 'origin' into elliott/resources

pull/17124/head
Elliott Johnson 2 weeks ago
commit 555e950f56

@ -1,5 +1,13 @@
# svelte
## 5.43.4
### Patch Changes
- chore: simplify connection/disconnection logic ([#17105](https://github.com/sveltejs/svelte/pull/17105))
- fix: reconnect deriveds to effect tree when time-travelling ([#17105](https://github.com/sveltejs/svelte/pull/17105))
## 5.43.3
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.43.3",
"version": "5.43.4",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -6,6 +6,13 @@ export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6;
export const BOUNDARY_EFFECT = 1 << 7;
/**
* Indicates that a reaction is connected to an effect root either it is an effect,
* or it is a derived that is depended on by at least one effect. If a derived has
* no dependents, we can disconnect it from the graph, allowing it to either be
* GC'd or reconnected later if an effect comes to depend on it again
*/
export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;
export const MAYBE_DIRTY = 1 << 12;
@ -26,8 +33,6 @@ export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
// Flags exclusive to deriveds
export const UNOWNED = 1 << 8;
export const DISCONNECTED = 1 << 9;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary

@ -9,15 +9,14 @@ import {
EFFECT_PRESERVED,
MAYBE_DIRTY,
STALE_REACTION,
UNOWNED,
ASYNC,
WAS_MARKED
WAS_MARKED,
CONNECTED
} from '#client/constants';
import {
active_reaction,
active_effect,
set_signal_status,
skip_reaction,
update_reaction,
increment_write_version,
set_active_effect,
@ -27,7 +26,7 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import * as w from '../warnings.js';
import { async_effect, destroy_effect, teardown } from './effects.js';
import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js';
import { eager_effects, internal_set, set_eager_effects, source } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -61,9 +60,7 @@ export function derived(fn) {
? /** @type {Derived} */ (active_reaction)
: null;
if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) {
flags |= UNOWNED;
} else {
if (active_effect !== null) {
// Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_PRESERVED;
@ -368,12 +365,16 @@ export function update_derived(derived) {
return;
}
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
if (batch_values !== null) {
batch_values.set(derived, derived.v);
// only cache the value if we're in a tracking context, otherwise we won't
// clear the cache in `mark_reactions` when dependencies are updated
if (effect_tracking()) {
batch_values.set(derived, derived.v);
}
} else {
var status =
(skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN;
var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN;
set_signal_status(derived, status);
}
}

@ -25,7 +25,6 @@ import {
ROOT_EFFECT,
EFFECT_TRANSPARENT,
DERIVED,
UNOWNED,
CLEAN,
EAGER_EFFECT,
HEAD_EFFECT,
@ -33,7 +32,8 @@ import {
EFFECT_PRESERVED,
STALE_REACTION,
USER_EFFECT,
ASYNC
ASYNC,
CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@ -57,11 +57,10 @@ export function validate_effect(rune) {
* @returns {'effect_orphan' | 'effect_in_unowned_derived' | 'effect_in_teardown' | null}
*/
export function get_effect_validation_error_code() {
if (active_effect === null && active_reaction === null) {
return 'effect_orphan';
}
if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) {
if (active_effect === null) {
if (active_reaction === null) {
return 'effect_orphan';
}
return 'effect_in_unowned_derived';
}
@ -114,7 +113,7 @@ function create_effect(type, fn, sync, push = true) {
deps: null,
nodes_start: null,
nodes_end: null,
f: type | DIRTY,
f: type | DIRTY | CONNECTED,
first: null,
fn,
last: null,

@ -23,18 +23,18 @@ import {
DIRTY,
BRANCH_EFFECT,
EAGER_EFFECT,
UNOWNED,
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC,
WAS_MARKED
WAS_MARKED,
CONNECTED
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch, eager_block_effects, schedule_effect } from './batch.js';
import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
@ -211,7 +211,8 @@ export function internal_set(source, value) {
if ((source.f & DIRTY) !== 0) {
execute_derived(/** @type {Derived} */ (source));
}
set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY);
set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY);
}
source.wv = increment_write_version();
@ -333,9 +334,17 @@ function mark_reactions(signal, status) {
}
if ((flags & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (reaction);
batch_values?.delete(derived);
if ((flags & WAS_MARKED) === 0) {
reaction.f |= WAS_MARKED;
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
// Only connected deriveds can be reliably unmarked right away
if (flags & CONNECTED) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY);
}
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) {

@ -4,6 +4,7 @@ import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'
import {
destroy_block_effect_children,
destroy_effect_children,
effect_tracking,
execute_effect_teardown
} from './reactivity/effects.js';
import {
@ -11,13 +12,12 @@ import {
MAYBE_DIRTY,
CLEAN,
DERIVED,
UNOWNED,
DESTROYED,
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
DISCONNECTED,
CONNECTED,
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE,
@ -137,10 +137,6 @@ export function set_update_version(value) {
update_version = value;
}
// If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction.
export let skip_reaction = false;
export function increment_write_version() {
return ++write_version;
}
@ -158,55 +154,18 @@ export function is_dirty(reaction) {
return true;
}
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = reaction.deps;
var is_unowned = (flags & UNOWNED) !== 0;
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if (dependencies !== null) {
var i;
var dependency;
var is_disconnected = (flags & DISCONNECTED) !== 0;
var is_unowned_connected = is_unowned && active_effect !== null && !skip_reaction;
var length = dependencies.length;
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
// then we need to re-connect the reaction to the dependency, unless the effect has already been destroyed
// (which can happen if the derived is read by an async derived)
if (
(is_disconnected || is_unowned_connected) &&
(active_effect === null || (active_effect.f & DESTROYED) === 0)
) {
var derived = /** @type {Derived} */ (reaction);
var parent = derived.parent;
for (i = 0; i < length; i++) {
dependency = dependencies[i];
// We always re-add all reactions (even duplicates) if the derived was
// previously disconnected, however we don't if it was unowned as we
// de-duplicate dependencies in that case
if (is_disconnected || !dependency?.reactions?.includes(derived)) {
(dependency.reactions ??= []).push(derived);
}
}
if (is_disconnected) {
derived.f ^= DISCONNECTED;
}
// If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
// and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
// flag
if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
derived.f ^= UNOWNED;
}
}
for (i = 0; i < length; i++) {
dependency = dependencies[i];
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency));
@ -218,9 +177,12 @@ export function is_dirty(reaction) {
}
}
// Unowned signals should never be marked as clean unless they
// are used within an active_effect without skip_reaction
if (!is_unowned || (active_effect !== null && !skip_reaction)) {
if (
(flags & CONNECTED) !== 0 &&
// During time traveling we don't want to reset the status so that
// traversal of the graph in the other batches still happens
batch_values === null
) {
set_signal_status(reaction, CLEAN);
}
}
@ -263,7 +225,6 @@ export function update_reaction(reaction) {
var previous_skipped_deps = skipped_deps;
var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction;
var previous_sources = current_sources;
var previous_component_context = component_context;
var previous_untracking = untracking;
@ -274,8 +235,6 @@ export function update_reaction(reaction) {
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
skip_reaction =
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_sources = null;
@ -311,12 +270,7 @@ export function update_reaction(reaction) {
reaction.deps = deps = new_deps;
}
if (
!skip_reaction ||
// Deriveds that already have reactions can cleanup, so we still add them as reactions
((flags & DERIVED) !== 0 &&
/** @type {import('#client').Derived} */ (reaction).reactions !== null)
) {
if (is_updating_effect && effect_tracking() && (reaction.f & CONNECTED) !== 0) {
for (i = skipped_deps; i < deps.length; i++) {
(deps[i].reactions ??= []).push(reaction);
}
@ -373,7 +327,6 @@ export function update_reaction(reaction) {
skipped_deps = previous_skipped_deps;
untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction;
current_sources = previous_sources;
set_component_context(previous_component_context);
untracking = previous_untracking;
@ -415,9 +368,10 @@ function remove_reaction(signal, dependency) {
) {
set_signal_status(dependency, MAYBE_DIRTY);
// If we are working with a derived that is owned by an effect, then mark it as being
// disconnected.
if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) {
dependency.f ^= DISCONNECTED;
// disconnected and remove the mark flag, as it cannot be reliably removed otherwise
if ((dependency.f & CONNECTED) !== 0) {
dependency.f ^= CONNECTED;
dependency.f &= ~WAS_MARKED;
}
// Disconnect any reactions owned by this reaction
destroy_derived_effects(/** @type {Derived} **/ (dependency));
@ -564,10 +518,7 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else if (!skip_reaction || !new_deps.includes(signal)) {
// Normally we can push duplicated dependencies to `new_deps`, but if we're inside
// an unowned derived because skip_reaction is true, then we need to ensure that
// we don't have duplicates
} else if (!new_deps.includes(signal)) {
new_deps.push(signal);
}
}
@ -585,20 +536,6 @@ export function get(signal) {
}
}
}
} else if (
is_derived &&
/** @type {Derived} */ (signal).deps === null &&
/** @type {Derived} */ (signal).effects === null
) {
var derived = /** @type {Derived} */ (signal);
var parent = derived.parent;
if (parent !== null && (parent.f & UNOWNED) === 0) {
// If the derived is owned by another derived then mark it as unowned
// as the derived value might have been referenced in a different context
// since and thus its parent might not be its true owner anymore
derived.f ^= UNOWNED;
}
}
if (DEV) {
@ -657,7 +594,7 @@ export function get(signal) {
}
if (is_derived) {
derived = /** @type {Derived} */ (signal);
var derived = /** @type {Derived} */ (signal);
var value = derived.v;
@ -684,9 +621,11 @@ export function get(signal) {
if (is_dirty(derived)) {
update_derived(derived);
}
}
if (batch_values?.has(signal)) {
if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) {
reconnect(derived);
}
} else if (batch_values?.has(signal)) {
return batch_values.get(signal);
}
@ -697,6 +636,25 @@ export function get(signal) {
return signal.v;
}
/**
* (Re)connect a disconnected derived, so that it is notified
* of changes in `mark_reactions`
* @param {Derived} derived
*/
function reconnect(derived) {
if (derived.deps === null) return;
derived.f ^= CONNECTED;
for (const dep of derived.deps) {
(dep.reactions ??= []).push(derived);
if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) {
reconnect(/** @type {Derived} */ (dep));
}
}
}
/** @param {Derived} derived */
function depends_on_old_values(derived) {
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.43.3';
export const VERSION = '5.43.4';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,27 @@
<script>
import { untrack } from "svelte";
let { double } = $props();
// Test setup:
// - component initialized while pending work
// - derived that depends on mulitple sources
// - indirect updates to subsequent deriveds
// - two sibling effects where the former influences the latter
// - first effect reads derived of second inside untrack
let x = $state(0);
const other = $derived(double + x);
const another = $derived(other + 1);
const another2 = $derived(another + 1);
$effect(() => {
untrack(() => {
another2;
x++
});
});
$effect(() => {
console.log(another2);
})
</script>

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const button = target.querySelector('button');
button?.click();
await tick();
assert.deepEqual(logs, [5]);
button?.click();
await tick();
assert.deepEqual(logs, [5, 7]);
}
});

@ -0,0 +1,19 @@
<script>
import Component from './Component.svelte';
let count = $state(0);
const double = $derived(count * 2);
</script>
<svelte:boundary>
{await new Promise((r) => {
// long enough for the test to do all its other stuff while this is pending
setTimeout(r, 10);
})}
{#snippet pending()}{/snippet}
</svelte:boundary>
<button onclick={() => count += 1}>{count}</button>
{#if count > 0}
<Component {double} />
{/if}

@ -0,0 +1,6 @@
<script>
let { double } = $props();
double; // derived is first read outside an active_reaction
</script>
<p>{double}</p>

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const button = target.querySelector('button');
button?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>1</button>
<p>2</p>
`
);
button?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>2</button>
<p>4</p>
`
);
}
});

@ -0,0 +1,19 @@
<script>
import Component from './Component.svelte';
let count = $state(0);
const double = $derived(count * 2);
</script>
<svelte:boundary>
{await new Promise((r) => {
// long enough for the test to do all its other stuff while this is pending
setTimeout(r, 10);
})}
{#snippet pending()}{/snippet}
</svelte:boundary>
<button onclick={() => count += 1}>{count}</button>
{#if count > 0}
<Component {double} />
{/if}
Loading…
Cancel
Save