Repository: sveltejs/svelte (JavaScript)
Date: 2020-01-24 22:27:39+00:00
Task type: Bug Fix

Issue:
"Maximum call stack size exceeded"
<!--
Thanks for raising an issue! (For *questions*, we recommend instead using https://stackoverflow.com and adding the 'svelte' tag.)

To help us help you, if you've found a bug please consider the following:

* If you can demonstrate the bug using https://svelte.dev/repl, please do.
* If that's not possible, we recommend creating a small repo that illustrates the problem.
* Make sure you include information about the browser, and which version of Svelte you're using

Reproductions should be small, self-contained, correct examples – http://sscce.org.

Occasionally, this won't be possible, and that's fine – we still appreciate you raising the issue. But please understand that Svelte is run by unpaid volunteers in their free time, and issues that follow these instructions will get fixed faster.

If you have a stack trace to include, we recommend putting inside a `<details>` block for the sake of the thread's readability:

<details>
  <summary>Stack trace</summary>

  Stack trace goes here...
</details>
-->

Recursion bug on svelte > 3.6.6 when creating a component using client-side component API. To reproduce: https://svelte.dev/repl/6fbeea34d77f43b9a6d57e1da71668fc?version=3.6.7
This works on 3.5.4.

<details>
  <summary>Error</summary>
  bundle.js:18532 Uncaught (in promise) RangeError: Maximum call stack size exceeded
</details>


Issue discussion:
This seems to have been introduced in #3150 — when `flush` is called while it is already running, `render_callbacks` are run multiple times.

We might be able to get away with something like `if (is_flushing) return`, but I'm not sure if there are cases where `render_callbacks` is modified with the expectation that the [`dirty_components`' update](https://github.com/sveltejs/svelte/blob/2b79a2269d57ba8b63a8422937e3e016a03c43bb/src/runtime/internal/scheduler.ts#L43) logic runs first.

The safer thing in that case would be to freeze `render_callbacks` per loop I guess?
I think I ran into this one too.
Trying to replace dom-elements (coming from .md in my real app) with svelte-components.
I'm pretty sure it worked with ~~3.5.1~~ (tried up to) 3.6.3  before updating to 3.6.7.

https://svelte.dev/repl/f3cb47560fcd4decbcabfd42e376607b?version=3.6.3
https://svelte.dev/repl/f3cb47560fcd4decbcabfd42e376607b?version=3.6.7
Was about to file a new issue, when I stumbled upon this one.
My app worked fine until version _3.6.3_, I am running into this issue since _3.6.4_

## My Observation
This issue occurs when you create a child component using client side api `new Component(options)` **inside** parent's life cycle functions(like onMount, afterUpdate, etc)

## Simple repro scenario
😃  https://svelte.dev/repl/d90c6f4e19594c28a4e454b5016570e2?version=3.6.3
🤕  https://svelte.dev/repl/d90c6f4e19594c28a4e454b5016570e2?version=3.6.4



Hideous hack but works until a fix is released...
```js
import { onMount, tick } from 'svelte';

onMount(async() => { 
    await tick();
    new RedChild({ target: document.body })
})
```
https://svelte.dev/repl/3692752c2bfc4c3f9292bdf84023f852?version=3.6.7
I bumped into this as well while trying to fiddle together Svelte and Google Maps API.

Reproduction here: https://svelte.dev/repl/80e53830f44c46b895376d1de1c08c93?version=3.12.1
Also running in to this issue in > 3.6.3. My issue stems from large documents generated from Markdown with numerous classes (like for highlighting code). Reverting to 3.6.3 does not throw the Maximum call stack size exceeded error. This error is thrown in the `compile` function.

Solution patch:
diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts
--- a/src/runtime/internal/scheduler.ts
+++ b/src/runtime/internal/scheduler.ts
@@ -31,8 +31,8 @@ export function add_flush_callback(fn) {
 	flush_callbacks.push(fn);
 }
 
+const seen_callbacks = new Set();
 export function flush() {
-	const seen_callbacks = new Set();
 
 	do {
 		// first, call beforeUpdate functions
@@ -52,10 +52,10 @@ export function flush() {
 			const callback = render_callbacks[i];
 
 			if (!seen_callbacks.has(callback)) {
-				callback();
-
 				// ...so guard against infinite loops
 				seen_callbacks.add(callback);
+
+				callback();
 			}
 		}
 
@@ -67,6 +67,7 @@ export function flush() {
 	}
 
 	update_scheduled = false;
+	seen_callbacks.clear();
 }
 
 function update($$) {


Test patch:
diff --git a/test/runtime/samples/lifecycle-onmount-infinite-loop/Child.svelte b/test/runtime/samples/lifecycle-onmount-infinite-loop/Child.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/lifecycle-onmount-infinite-loop/Child.svelte
@@ -0,0 +1 @@
+Child
diff --git a/test/runtime/samples/lifecycle-onmount-infinite-loop/_config.js b/test/runtime/samples/lifecycle-onmount-infinite-loop/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/lifecycle-onmount-infinite-loop/_config.js
@@ -0,0 +1,6 @@
+export default {
+	test({ assert, component }) {
+		const { count } = component;
+		assert.deepEqual(count, 1);
+	}
+};
diff --git a/test/runtime/samples/lifecycle-onmount-infinite-loop/main.svelte b/test/runtime/samples/lifecycle-onmount-infinite-loop/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/lifecycle-onmount-infinite-loop/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	import { onMount } from 'svelte';
+	import Child from './Child.svelte';
+
+	let root;
+	export let count = 0;
+
+	onMount(() => {
+		if (count < 5) {
+			count++;
+			new Child({ target: root });
+		}
+	});
+</script>
+
+<div bind:this={root}></div>