WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changeset/loud-rabbits-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: ensure unresolved async deriveds return undefined instead of internal symbol
5 changes: 3 additions & 2 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -642,6 +646,10 @@ export function get(signal) {
throw signal.v;
}

if (signal.v === UNINITIALIZED) {
return /** @type {V} */ (undefined);
}

return signal.v;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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}
Original file line number Diff line number Diff line change
@@ -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>
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
</script>

<svelte:boundary>
{#snippet pending()}
<p>Loading...</p>
{/snippet}

<Child />
</svelte:boundary>
115 changes: 112 additions & 3 deletions packages/svelte/tests/signals/test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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