pull/17373/merge
David 2 days ago committed by GitHub
commit 1758b9743c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure unresolved async deriveds return undefined instead of internal symbol

@ -21,6 +21,7 @@ import {
MANAGED_EFFECT
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { UNINITIALIZED } from '../../../constants.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
@ -320,7 +321,7 @@ export class Batch {
// Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get`
if ((source.f & ERROR_VALUE) === 0) {
this.current.set(source, source.v);
batch_values?.set(source, source.v);
batch_values?.set(source, source.v === UNINITIALIZED ? undefined : source.v);
}
}
@ -546,7 +547,7 @@ export class Batch {
for (const [source, previous] of batch.previous) {
if (!batch_values.has(source)) {
batch_values.set(source, previous);
batch_values.set(source, previous === UNINITIALIZED ? undefined : previous);
}
}
}

@ -163,6 +163,10 @@ export function is_dirty(reaction) {
}
if (flags & DERIVED) {
// UNINITIALIZED deriveds need computation
if (/** @type {Derived} */ (reaction).v === UNINITIALIZED) {
return true;
}
reaction.f &= ~WAS_MARKED;
}
@ -642,6 +646,10 @@ export function get(signal) {
throw signal.v;
}
if (signal.v === UNINITIALIZED) {
return /** @type {V} */ (undefined);
}
return signal.v;
}

@ -0,0 +1,18 @@
<script>
let foo = $state(null);
$effect(() => {
foo = 69;
});
let bar = $derived(await 1);
let baz = $derived(foo ? foo * bar : null);
const qux = "qux";
</script>
<p id="bar">bar: {bar}</p>
<p id="baz">baz: {baz}</p>
{#if qux}
<p></p>
{/if}

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
// Ensure async derived remains reactive with associated effect and boundary with guard (#17271)
//
// Accounts for both UNINITIALIZED leaking from get and the baz derived remaining NaN
// due to having both an $effect and a boundary with an if in the same template
export default test({
html: `<p>Loading...</p>`,
skip_no_async: true,
async test({ assert, target }) {
await tick();
assert.htmlEqual(
target.innerHTML,
`
<p id="bar">bar: 1</p>
<p id="baz">baz: 69</p>
<p></p>
`
);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
</script>
<svelte:boundary>
{#snippet pending()}
<p>Loading...</p>
{/snippet}
<Child />
</svelte:boundary>

@ -1,5 +1,5 @@
import { describe, assert, it } from 'vitest';
import { flushSync } from '../../src/index-client';
import { flushSync, fork } from '../../src/index-client';
import * as $ from '../../src/internal/client/runtime';
import { push, pop } from '../../src/internal/client/context';
import {
@ -9,15 +9,24 @@ import {
user_effect,
user_pre_effect
} from '../../src/internal/client/reactivity/effects';
import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources';
import {
state,
set,
update,
update_pre,
internal_set,
source
} from '../../src/internal/client/reactivity/sources';
import type { Derived, Effect, Source, Value } from '../../src/internal/client/types';
import { proxy } from '../../src/internal/client/proxy';
import { derived } from '../../src/internal/client/reactivity/deriveds';
import { snapshot } from '../../src/internal/shared/clone.js';
import { SvelteSet } from '../../src/reactivity/set';
import { DESTROYED } from '../../src/internal/client/constants';
import { DESTROYED, MAYBE_DIRTY } from '../../src/internal/client/constants';
import { UNINITIALIZED } from '../../src/constants';
import { noop } from 'svelte/internal/client';
import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags';
import { Batch } from '../../src/internal/client/reactivity/batch';
/**
* @param runes runes mode
@ -1493,4 +1502,104 @@ describe('signals', () => {
assert.deepEqual(log, ['inner destroyed', 'inner destroyed']);
};
});
// prevent UNINITIALIZED from leaking in get() (#17271)
it('derived with deps evaluated in fork should not return UNINITIALIZED', () => {
enable_async_mode_flag();
try {
const count = state(5);
let d: Derived<number>;
const f = fork(() => {
effect_root(() => {
render_effect(() => {
d = derived(() => $.get(count) * 2);
$.get(d);
});
})();
});
assert.equal(d!.v, UNINITIALIZED as unknown);
assert.equal((d!.f & MAYBE_DIRTY) !== 0, true);
assert.equal($.get(d!), 10);
f.discard();
} finally {
disable_async_mode_flag();
}
});
it('should return undefined when reading UNINITIALIZED from batch.previous via apply()', () => {
enable_async_mode_flag();
try {
const s = source(UNINITIALIZED as unknown);
const dummy = state(0);
const forkA = fork(() => {
effect_root(() => {
render_effect(() => {
internal_set(s, 'resolved');
});
})();
});
let readValue: any;
const forkB = fork(() => {
effect_root(() => {
render_effect(() => {
set(dummy, 1);
readValue = $.get(s);
});
})();
});
assert.equal(readValue, undefined);
assert.notEqual(readValue, UNINITIALIZED);
forkA.discard();
forkB.discard();
} finally {
disable_async_mode_flag();
}
});
it('should return undefined when reading UNINITIALIZED source captured in concurrent batch', () => {
enable_async_mode_flag();
try {
const s = source('initial');
const dummy = state(0);
// Create a pending batch
const f1 = fork(() => {
effect_root(() => {
render_effect(() => {
set(dummy, 1);
});
})();
});
let readValue: any;
const f2 = fork(() => {
effect_root(() => {
render_effect(() => {
// Puts UNINITIALIZED in batch_values via capture()
internal_set(s, UNINITIALIZED as unknown);
readValue = $.get(s);
});
})();
});
assert.equal(readValue, undefined);
assert.notEqual(readValue, UNINITIALIZED);
f1.discard();
f2.discard();
} finally {
disable_async_mode_flag();
}
});
});

Loading…
Cancel
Save