Skip to content
Merged
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
26 changes: 6 additions & 20 deletions packages/svelte/src/internal/client/reactivity/deriveds.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,32 +113,24 @@ export function async_derived(fn, location) {
// only suspend in async deriveds created on initialisation
var should_suspend = !active_reaction;

/** @type {Map<Batch, ReturnType<typeof deferred<V>> & { rejected?: boolean }>} */
/** @type {Map<Batch, ReturnType<typeof deferred<V>>>} */
var deferreds = new Map();

async_effect(() => {
if (DEV) current_async_effect = active_effect;

/** @type {ReturnType<typeof deferred<V>> & { rejected?: boolean }} */
/** @type {ReturnType<typeof deferred<V>>} */
var d = deferred();
promise = d.promise;

try {
// If this code is changed at some point, make sure to still access the then property
// of fn() to read any signals it might access, so that we track them as dependencies.
Promise.resolve(fn()).then((v) => {
if (d.rejected) {
// If we rejected this stale promise, d.resolve
// is a noop (d.promise.then(handler) below will never run).
// In this case we need to unset the restored context here
// to avoid leaking it (and e.g. cause false-positive mutation errors).
unset_context();
} else {
d.resolve(v);
}
}, d.reject);
// We call `unset_context` to undo any `save` calls that happen inside `fn()`
Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context);
Copy link
Member

@dummdidumm dummdidumm Oct 13, 2025

Choose a reason for hiding this comment

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

It would be good to have a comment explaining why we want to have it up here in addition to the handler below, but otherwise lgtm 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

actually you know what? we don't need the unset_context below, just here (and in the catch clause)

Copy link
Member

@dummdidumm dummdidumm Oct 13, 2025

Choose a reason for hiding this comment

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

I don't think so - when you reject a stale promise you'll end up in the handler and need to unset the context there because it doesn't run this line. The previous version was more correct

Copy link
Member Author

Choose a reason for hiding this comment

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

Why? The point of #16935 is that context is saved in fn() but may not be correctly unset if the promise is forcibly rejected before fn() resolves. When that happens, there's nothing to unset, surely?

Copy link
Member

Choose a reason for hiding this comment

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

Most likely yes, if everything's correct elsewhere it should be null going in. So yeah, I guess your change is fine

} catch (error) {
d.reject(error);
unset_context();
}

if (DEV) current_async_effect = null;
Expand All @@ -151,11 +143,7 @@ export function async_derived(fn, location) {
if (!pending) {
batch.increment();

var previous_deferred = deferreds.get(batch);
if (previous_deferred) {
previous_deferred.rejected = true;
previous_deferred.reject(STALE_REACTION);
}
deferreds.get(batch)?.reject(STALE_REACTION);
deferreds.set(batch, d);
}
}
Expand Down Expand Up @@ -199,8 +187,6 @@ export function async_derived(fn, location) {
boundary.update_pending_count(-1);
if (!pending) batch.decrement();
}

unset_context();
};

d.promise.then(handler, (e) => handler(null, e || 'unknown'));
Expand Down
Loading