From 64fa513e8bf2ef9b7adb3d177e5083ebb80be5bb Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:50:01 -0700 Subject: [PATCH 01/10] init --- .../phases/2-analyze/visitors/CallExpression.js | 1 + .../3-transform/client/visitors/CallExpression.js | 3 +++ .../3-transform/server/visitors/CallExpression.js | 2 +- packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/effects.js | 14 ++++++++++++++ packages/svelte/src/utils.js | 1 + 6 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 9b6337b9ed9a..25cff4bfe093 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -150,6 +150,7 @@ export function CallExpression(node, context) { break; + case '$effect.active': case '$effect.tracking': if (node.arguments.length !== 0) { e.rune_invalid_arguments(node, rune); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 3e2f1414e63b..746c051e3f30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -17,6 +17,9 @@ export function CallExpression(node, context) { case '$host': return b.id('$$props.$$host'); + case '$effect.active': + return b.call('$.effect_active'); + case '$effect.tracking': return b.call('$.effect_tracking'); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 41d3202ce9ea..64d4f573c586 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -16,7 +16,7 @@ export function CallExpression(node, context) { return b.void0; } - if (rune === '$effect.tracking') { + if (rune === '$effect.tracking' || rune === '$effect.active') { return b.false; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index cddb432a982b..a186cdd86bbf 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -107,6 +107,7 @@ export { } from './reactivity/deriveds.js'; export { aborted, + effect_active, effect_tracking, effect_root, legacy_pre_effect, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d95..ccc2d9c9ec44 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -165,6 +165,20 @@ export function effect_tracking() { return active_reaction !== null && !untracking; } +/** + * Internal representation of `$effect.active()` + * @returns {boolean} + */ +export function effect_active() { + if (active_reaction === null && active_effect === null) { + return false; + } + if (is_destroying_effect) { + return false; + } + return true; +} + /** * @param {() => void} fn */ diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index cd79cfc27467..f49382921a5b 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -442,6 +442,7 @@ const RUNES = /** @type {const} */ ([ '$props.id', '$bindable', '$effect', + '$effect.active', '$effect.pre', '$effect.tracking', '$effect.root', From 45d3eeb4e6c905f44f1328200f7dc8e0c0e874b4 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Jul 2025 03:00:43 -0700 Subject: [PATCH 02/10] add types, DRY out some logic --- packages/svelte/src/ambient.d.ts | 28 ++++++++++++ .../svelte/src/internal/client/constants.js | 5 +++ .../src/internal/client/reactivity/effects.js | 43 +++++++++++++------ packages/svelte/types/index.d.ts | 28 ++++++++++++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 7c3b941ed1fb..aaaddbe544ff 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -237,6 +237,34 @@ declare namespace $derived { declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { + /** + * The `$effect.active` rune is an advanced feature that indicates whether an effect or async `$derived` can be created in the current context. + * Effects and async deriveds can only be created in root effects, which are created during component setup, or can be programmatically created via `$effect.root`. + * + * Example: + * ```svelte + * + * + * + * ``` + * + * https://svelte.dev/docs/svelte/$effect#$effect.active + */ + export function active(): boolean; /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is right before the DOM is updated. diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 50a7a21ae80f..de0ca92f1289 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -41,3 +41,8 @@ export const ELEMENT_NODE = 1; export const TEXT_NODE = 3; export const COMMENT_NODE = 8; export const DOCUMENT_FRAGMENT_NODE = 11; + +export const VALID_EFFECT_PARENT = 0; +export const EFFECT_ORPHAN = 1; +export const UNOWNED_DERIVED_PARENT = 2; +export const EFFECT_TEARDOWN = 3; \ No newline at end of file diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index ccc2d9c9ec44..7921b1e5e91f 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -33,7 +33,11 @@ import { EFFECT_PRESERVED, STALE_REACTION, USER_EFFECT, - ASYNC + ASYNC, + EFFECT_ORPHAN, + EFFECT_TEARDOWN, + UNOWNED_DERIVED_PARENT, + VALID_EFFECT_PARENT } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -44,19 +48,38 @@ import { Batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; /** - * @param {'$effect' | '$effect.pre' | '$inspect'} rune + * @returns {number} */ -export function validate_effect(rune) { +function active_root_effect() { if (active_effect === null && active_reaction === null) { - e.effect_orphan(rune); + return EFFECT_ORPHAN; } if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) { - e.effect_in_unowned_derived(); + return UNOWNED_DERIVED_PARENT; } if (is_destroying_effect) { - e.effect_in_teardown(rune); + return EFFECT_TEARDOWN; + } + + return VALID_EFFECT_PARENT; +} + +/** + * @param {'$effect' | '$effect.pre' | '$inspect'} rune + */ +export function validate_effect(rune) { + const valid_effect_parent = active_root_effect(); + switch(valid_effect_parent) { + case VALID_EFFECT_PARENT: + return; + case EFFECT_ORPHAN: + e.effect_orphan(rune); + case UNOWNED_DERIVED_PARENT: + e.effect_in_unowned_derived(); + case EFFECT_TEARDOWN: + e.effect_in_teardown(rune); } } @@ -170,13 +193,7 @@ export function effect_tracking() { * @returns {boolean} */ export function effect_active() { - if (active_reaction === null && active_effect === null) { - return false; - } - if (is_destroying_effect) { - return false; - } - return true; + return active_root_effect() === VALID_EFFECT_PARENT; } /** diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a8b769d6d4c3..abe0213ed354 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3311,6 +3311,34 @@ declare namespace $derived { declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { + /** + * The `$effect.active` rune is an advanced feature that indicates whether an effect or async `$derived` can be created in the current context. + * Effects and async deriveds can only be created in root effects, which are created during component setup, or can be programmatically created via `$effect.root`. + * + * Example: + * ```svelte + * + * + * + * ``` + * + * https://svelte.dev/docs/svelte/$effect#$effect.active + */ + export function active(): boolean; /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is right before the DOM is updated. From 9ae4a65c4ac1d8a0ba8069ac3a1da3889dca61a9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:26:56 -0700 Subject: [PATCH 03/10] add docs --- documentation/docs/02-runes/04-$effect.md | 24 +++++++++++++++++++ .../src/internal/client/reactivity/effects.js | 2 ++ 2 files changed, 26 insertions(+) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 5820e178a078..4a27a7277851 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -255,6 +255,30 @@ const destroy = $effect.root(() => { destroy(); ``` +## `$effect.active` + +The `$effect.active` rune is an advanced feature that indicates whether or not an effect or [async `$derived`](await-expressions) can be created in the current context. To improve performance and memory efficiency, effects and async deriveds can only be created when a root effect is active. Root effects are created during component setup, but they can also be programmatically created via `$effect.root`. + +```svelte + + + +``` + ## When not to use `$effect` In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this... diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 7921b1e5e91f..75f8aa22f3d3 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -48,6 +48,8 @@ import { Batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; /** + * If an effect can be created in the current context, `VALID_EFFECT_PARENT` is returned. + * If not, a value indicating why is returned. * @returns {number} */ function active_root_effect() { From c61f2a9bae8176b9e9aa74e4206c17720a82d8c9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:27:25 -0700 Subject: [PATCH 04/10] changeset --- .changeset/two-dancers-speak.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-dancers-speak.md diff --git a/.changeset/two-dancers-speak.md b/.changeset/two-dancers-speak.md new file mode 100644 index 000000000000..7044eaa329d3 --- /dev/null +++ b/.changeset/two-dancers-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `$effect.active` rune From 9c1de633c0812f47eca2e8c9c5c1366eb9f2212b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 10:41:47 -0700 Subject: [PATCH 05/10] add test --- packages/svelte/tests/signals/test.ts | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b16..261198fc1c87 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -4,6 +4,7 @@ import * as $ from '../../src/internal/client/runtime'; import { push, pop } from '../../src/internal/client/context'; import { effect, + effect_active, effect_root, render_effect, user_effect, @@ -1390,4 +1391,41 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.active()', () => { + const log: Array = []; + + return () => { + log.push('effect orphan', effect_active()); + const destroy = effect_root(() => { + log.push('effect root', effect_active()); + effect(() => { + log.push('effect', effect_active()); + }); + $.get( + derived(() => { + log.push('derived', effect_active()); + return 1; + }) + ); + return () => { + log.push('effect teardown', effect_active()); + }; + }); + flushSync(); + destroy(); + assert.deepEqual(log, [ + 'effect orphan', + false, + 'effect root', + true, + 'derived', + true, + 'effect', + true, + 'effect teardown', + false + ]); + }; + }); }); From c82fb3a0c83b7aed53183c61b26ab7a699089dfa Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:57:48 -0700 Subject: [PATCH 06/10] remove obsolete test --- .../compiler-errors/samples/effect-active-rune/_config.js | 8 -------- .../samples/effect-active-rune/main.svelte.js | 1 - 2 files changed, 9 deletions(-) delete mode 100644 packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js diff --git a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js b/packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js deleted file mode 100644 index e12e4046f36d..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'rune_renamed', - message: '`$effect.active` is now `$effect.tracking`' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js deleted file mode 100644 index c33c104aac31..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/effect-active-rune/main.svelte.js +++ /dev/null @@ -1 +0,0 @@ -$effect.active(); From 7b33b2ab54ce4e1710e89e1120a100e00ceaefc4 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 20 Jul 2025 14:04:46 -0700 Subject: [PATCH 07/10] fix lint --- .../svelte/src/internal/client/reactivity/effects.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 75f8aa22f3d3..5d0b24e33d4e 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -48,8 +48,8 @@ import { Batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; /** - * If an effect can be created in the current context, `VALID_EFFECT_PARENT` is returned. - * If not, a value indicating why is returned. + * If an effect can be created in the current context, `VALID_EFFECT_PARENT` is returned. + * If not, a value indicating why is returned. * @returns {number} */ function active_root_effect() { @@ -73,15 +73,18 @@ function active_root_effect() { */ export function validate_effect(rune) { const valid_effect_parent = active_root_effect(); - switch(valid_effect_parent) { + switch (valid_effect_parent) { case VALID_EFFECT_PARENT: return; case EFFECT_ORPHAN: e.effect_orphan(rune); + break; case UNOWNED_DERIVED_PARENT: e.effect_in_unowned_derived(); + break; case EFFECT_TEARDOWN: e.effect_in_teardown(rune); + break; } } From ab91795ac8b2bfefac3e72e2f24b6a0e40d16f91 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 20 Jul 2025 14:13:23 -0700 Subject: [PATCH 08/10] lint --- packages/svelte/src/ambient.d.ts | 6 +++--- packages/svelte/src/internal/client/constants.js | 2 +- packages/svelte/types/index.d.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 5bf69a157fe2..cc395995cc3c 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -240,12 +240,12 @@ declare namespace $effect { /** * The `$effect.active` rune is an advanced feature that indicates whether an effect or async `$derived` can be created in the current context. * Effects and async deriveds can only be created in root effects, which are created during component setup, or can be programmatically created via `$effect.root`. - * + * * Example: * ```svelte *