diff --git a/.changeset/twelve-stars-serve.md b/.changeset/twelve-stars-serve.md new file mode 100644 index 0000000000..6a4d7ecfa7 --- /dev/null +++ b/.changeset/twelve-stars-serve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reinstate reactivity loss tracking diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 31408ee8f9..e0f1f086e3 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -19,10 +19,10 @@ import { import { Batch, current_batch } from './batch.js'; import { async_derived, - current_async_effect, + reactivity_loss_tracker, derived, derived_safe_equal, - set_from_async_derived + set_reactivity_loss_tracker } from './deriveds.js'; import { aborted } from './effects.js'; @@ -120,7 +120,7 @@ export function capture() { if (activate_batch) previous_batch?.activate(); if (DEV) { - set_from_async_derived(null); + set_reactivity_loss_tracker(null); set_dev_stack(previous_dev_stack); } }; @@ -152,11 +152,11 @@ export async function save(promise) { * @returns {Promise<() => T>} */ 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; return () => { - set_from_async_derived(previous_async_effect); + set_reactivity_loss_tracker(previous_async_effect); return value; }; } @@ -213,7 +213,7 @@ export function unset_context(deactivate_batch = true) { if (deactivate_batch) current_batch?.deactivate(); if (DEV) { - set_from_async_derived(null); + set_reactivity_loss_tracker(null); set_dev_stack(null); } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 7df7651294..415a167f9c 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -36,7 +36,6 @@ import { import { eager_effects, internal_set, set_eager_effects, source } from './sources.js'; import { get_error } from '../../shared/dev.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; -import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { batch_values, current_batch } from './batch.js'; @@ -44,12 +43,16 @@ import { increment_pending, unset_context } from './async.js'; import { deferred, includes, noop } from '../../shared/utils.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 */ -export function set_from_async_derived(v) { - current_async_effect = v; +/** @param {{ effect: Effect, warned: boolean } | null} v */ +export function set_reactivity_loss_tracker(v) { + reactivity_loss_tracker = v; } export const recent_async_deriveds = new Set(); @@ -123,7 +126,12 @@ export function async_derived(fn, label, location) { var deferreds = new Map(); async_effect(() => { - if (DEV) current_async_effect = active_effect; + if (DEV) { + reactivity_loss_tracker = { + effect: /** @type {Effect} */ (active_effect), + warned: false + }; + } /** @type {ReturnType>} */ var d = deferred(); @@ -139,7 +147,9 @@ export function async_derived(fn, label, location) { unset_context(); } - if (DEV) current_async_effect = null; + if (DEV) { + reactivity_loss_tracker = null; + } var batch = /** @type {Batch} */ (current_batch); @@ -156,7 +166,9 @@ export function async_derived(fn, label, location) { * @param {unknown} error */ const handler = (value, error = undefined) => { - current_async_effect = null; + if (DEV) { + reactivity_loss_tracker = null; + } batch.activate(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3260c24b8c..906d68fbf0 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { } from './constants.js'; import { old_values } from './reactivity/sources.js'; import { - destroy_derived_effects, + reactivity_loss_tracker, execute_derived, freeze_derived_effects, recent_async_deriveds, @@ -58,6 +58,7 @@ import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js'; import { set_signal_status, update_derived_status } from './reactivity/status.js'; +import * as w from './warnings.js'; let is_updating_effect = false; @@ -568,19 +569,20 @@ export function get(signal) { } if (DEV) { - // TODO reinstate this, but make it actually work - // if (current_async_effect) { - // var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; - // var was_read = current_async_effect.deps?.includes(signal); + if ( + !untracking && + reactivity_loss_tracker && + !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'); - // // eslint-disable-next-line no-console - // if (trace) console.warn(trace); - // } - // } + var trace = get_error('traced at'); + // eslint-disable-next-line no-console + if (trace) console.warn(trace); + } recent_async_deriveds.delete(signal); @@ -595,7 +597,7 @@ export function get(signal) { if (signal.trace) { signal.trace(); } else { - var trace = get_error('traced at'); + trace = get_error('traced at'); if (trace) { var entry = tracing_expressions.entries.get(signal); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index ce7cd6bd49..a5dd7fa28a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -1,10 +1,8 @@ import { tick } from 'svelte'; import { test } from '../../test'; +import { normalise_trace_logs } from '../../../helpers.js'; export default test({ - // TODO reinstate - skip: true, - compileOptions: { dev: true }, @@ -15,13 +13,10 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

3

'); - assert.equal( - warnings[0], - 'Detected reactivity loss when reading `values[1]`. 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); + assert.deepEqual(normalise_trace_logs(warnings), [ + { + log: 'Detected reactivity loss when reading `values.length`. This happens when state is read in an async function after an earlier `await`' + } + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index ad333a573a..747648e83f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -2,9 +2,6 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - // TODO reinstate this - skip: true, - compileOptions: { dev: true },