fix: reinstate reactivity loss tracking (#17801)

We commented out this code in #17038 because it was broken. I suspect it
was broken because we weren't correctly calling `unset_context` inside
`run`, leading to false positives — this is now fixed, and as such I
_think_ we can safely reinstate it.

One small change — I got rid of the `was_read` check. I assume this
existed to prevent duplicate warnings, but it actually causes false
negatives in the case where you read a signal while the reaction is
being tracked then again while it's untracked.

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

- [x] 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
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [ ] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

---------

Co-authored-by: Tee Ming <chewteeming01@gmail.com>
pull/17953/head
Rich Harris 6 months ago committed by GitHub
parent 6a303c3638
commit 5faf102782
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: reinstate reactivity loss tracking

@ -19,10 +19,10 @@ import {
import { Batch, current_batch } from './batch.js'; import { Batch, current_batch } from './batch.js';
import { import {
async_derived, async_derived,
current_async_effect, reactivity_loss_tracker,
derived, derived,
derived_safe_equal, derived_safe_equal,
set_from_async_derived set_reactivity_loss_tracker
} from './deriveds.js'; } from './deriveds.js';
import { aborted } from './effects.js'; import { aborted } from './effects.js';
@ -131,7 +131,7 @@ export function capture() {
} }
if (DEV) { if (DEV) {
set_from_async_derived(null); set_reactivity_loss_tracker(null);
set_dev_stack(previous_dev_stack); set_dev_stack(previous_dev_stack);
} }
}; };
@ -163,11 +163,11 @@ export async function save(promise) {
* @returns {Promise<() => T>} * @returns {Promise<() => T>}
*/ */
export async function track_reactivity_loss(promise) { export async function track_reactivity_loss(promise) {
var previous_async_effect = current_async_effect; var previous_async_effect = reactivity_loss_tracker;
var value = await promise; var value = await promise;
return () => { return () => {
set_from_async_derived(previous_async_effect); set_reactivity_loss_tracker(previous_async_effect);
return value; return value;
}; };
} }
@ -224,7 +224,7 @@ export function unset_context(deactivate_batch = true) {
if (deactivate_batch) current_batch?.deactivate(); if (deactivate_batch) current_batch?.deactivate();
if (DEV) { if (DEV) {
set_from_async_derived(null); set_reactivity_loss_tracker(null);
set_dev_stack(null); set_dev_stack(null);
} }
} }

@ -45,12 +45,16 @@ import { increment_pending, unset_context } from './async.js';
import { deferred, includes, noop } from '../../shared/utils.js'; import { deferred, includes, noop } from '../../shared/utils.js';
import { set_signal_status, update_derived_status } from './status.js'; import { set_signal_status, update_derived_status } from './status.js';
/** @type {Effect | null} */ /**
export let current_async_effect = null; * This allows us to track 'reactivity loss' that occurs when signals
* are read after a non-context-restoring `await`. Dev-only
* @type {{ effect: Effect, warned: boolean } | null}
*/
export let reactivity_loss_tracker = null;
/** @param {Effect | null} v */ /** @param {{ effect: Effect, warned: boolean } | null} v */
export function set_from_async_derived(v) { export function set_reactivity_loss_tracker(v) {
current_async_effect = v; reactivity_loss_tracker = v;
} }
export const recent_async_deriveds = new Set(); export const recent_async_deriveds = new Set();
@ -124,7 +128,12 @@ export function async_derived(fn, label, location) {
var deferreds = new Map(); var deferreds = new Map();
async_effect(() => { async_effect(() => {
if (DEV) current_async_effect = active_effect; if (DEV) {
reactivity_loss_tracker = {
effect: /** @type {Effect} */ (active_effect),
warned: false
};
}
var effect = /** @type {Effect} */ (active_effect); var effect = /** @type {Effect} */ (active_effect);
@ -142,7 +151,9 @@ export function async_derived(fn, label, location) {
unset_context(); unset_context();
} }
if (DEV) current_async_effect = null; if (DEV) {
reactivity_loss_tracker = null;
}
var batch = /** @type {Batch} */ (current_batch); var batch = /** @type {Batch} */ (current_batch);
@ -174,7 +185,9 @@ export function async_derived(fn, label, location) {
* @param {unknown} error * @param {unknown} error
*/ */
const handler = (value, error = undefined) => { const handler = (value, error = undefined) => {
if (DEV) current_async_effect = null; if (DEV) {
reactivity_loss_tracker = null;
}
if (decrement_pending) { if (decrement_pending) {
// don't trigger an update if we're only here because // don't trigger an update if we're only here because

@ -27,7 +27,7 @@ import {
} from './constants.js'; } from './constants.js';
import { old_values } from './reactivity/sources.js'; import { old_values } from './reactivity/sources.js';
import { import {
destroy_derived_effects, reactivity_loss_tracker,
execute_derived, execute_derived,
freeze_derived_effects, freeze_derived_effects,
recent_async_deriveds, recent_async_deriveds,
@ -58,6 +58,7 @@ import { UNINITIALIZED } from '../../constants.js';
import { captured_signals } from './legacy.js'; import { captured_signals } from './legacy.js';
import { without_reactive_context } from './dom/elements/bindings/shared.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js';
import { set_signal_status, update_derived_status } from './reactivity/status.js'; import { set_signal_status, update_derived_status } from './reactivity/status.js';
import * as w from './warnings.js';
let is_updating_effect = false; let is_updating_effect = false;
@ -568,19 +569,20 @@ export function get(signal) {
} }
if (DEV) { if (DEV) {
// TODO reinstate this, but make it actually work if (
// if (current_async_effect) { !untracking &&
// var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; reactivity_loss_tracker &&
// var was_read = current_async_effect.deps?.includes(signal); !reactivity_loss_tracker.warned &&
(reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0
) {
reactivity_loss_tracker.warned = true;
// if (!tracking && !untracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label));
// w.await_reactivity_loss(/** @type {string} */ (signal.label));
// var trace = get_error('traced at'); var trace = get_error('traced at');
// // eslint-disable-next-line no-console // eslint-disable-next-line no-console
// if (trace) console.warn(trace); if (trace) console.warn(trace);
// } }
// }
recent_async_deriveds.delete(signal); recent_async_deriveds.delete(signal);
@ -595,7 +597,7 @@ export function get(signal) {
if (signal.trace) { if (signal.trace) {
signal.trace(); signal.trace();
} else { } else {
var trace = get_error('traced at'); trace = get_error('traced at');
if (trace) { if (trace) {
var entry = tracing_expressions.entries.get(signal); var entry = tracing_expressions.entries.get(signal);

@ -1,10 +1,8 @@
import { tick } from 'svelte'; import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({ export default test({
// TODO reinstate
skip: true,
compileOptions: { compileOptions: {
dev: true dev: true
}, },
@ -15,13 +13,10 @@ export default test({
await tick(); await tick();
assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>'); assert.htmlEqual(target.innerHTML, '<button>a</button><button>b</button><h1>3</h1>');
assert.equal( assert.deepEqual(normalise_trace_logs(warnings), [
warnings[0], {
'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' log: 'Detected reactivity loss when reading `values.length`. This happens when state is read in an async function after an earlier `await`'
); }
]);
assert.equal(warnings[1].name, 'traced at');
assert.equal(warnings.length, 2);
} }
}); });

@ -2,9 +2,6 @@ import { tick } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
// TODO reinstate this
skip: true,
compileOptions: { compileOptions: {
dev: true dev: true
}, },

Loading…
Cancel
Save