Error too much recursion on setInterval ajax

Greets everyone,

hope you can help me out with this:

I wanted to update JS object data by jquery (via $.ajax) by receiving PHP’s mysqli_fetch_assoc array. It worked until I started using setInterval to update data every 15 minutes. Since then I get the following error: Uncaught (in promise) InternalError: too much recursion.

Also, tried only calling one object update (replacing for…of for a certain objectID), but I had no success. The error occurs on the line of $.ajax statement. Tell me if you need more information.

Even adding a counter to have 1 second between two $.ajax calls, does not work.

Additionally initalizing count=1 instead of 0, makes no difference.

Error: Uncaught (in promise) InternalError: too much recursion

aUpdateObject Updater.js:15
updateObject Updater.js:8
interval Updater.js:35
(Async: setTimeout handler)
interval Updater.js:32
(Async: setInterval handler)
initUpdater Updater.js:30
setup Central.js:29
onFinish Central.js:28
_decreaseWaitingTasksCount assetsManager.ts:1143
done assetsManager.ts:1176
_onDoneCallback assetsManager.ts:168
run assetsManager.ts:117
runTask assetsManager.ts:389
successHandler sceneLoader.ts:756
ImportMesh sceneLoader.ts:792
(Async: promise callback)
ImportMesh sceneLoader.ts:789
dataCallback sceneLoader.ts:552
GLTFFileLoader glTFFileLoader.ts:652
LoadFile fileTools.ts:343
onReadyStateChange fileTools.ts:457
(Async: EventListener.handleEvent)
addEventListener webRequest.ts:138
retryLoop fileTools.ts:482
requestFile fileTools.ts:485
RequestFile fileTools.ts:519
LoadFile fileTools.ts:342
_loadFile scene.ts:4416
_loadFile glTFFileLoader.ts:866
loadFile glTFFileLoader.ts:647
manifestChecked sceneLoader.ts:582
Database database.ts:69
OfflineProviderFactory database.ts:11
_LoadData sceneLoader.ts:603
ImportMesh sceneLoader.ts:765
runTask assetsManager.ts:379
run assetsManager.ts:114
_runTask assetsManager.ts:1194
load assetsManager.ts:1248
createScene Central.js:36
default Central.js:10
<anonym> index.js:5
InnerModuleEvaluation self-hosted:2325
evaluation self-hosted:2286

>jquery 128
    isPlainObject
    ... (127x?) extend

File: Updater.js

    export class Updater {
        constructor(main) {
    	 	this.objects = {};
    	 	this.upobjects = {};	// update-requests-array
    	}

		updateObject(id) {
			if(!this.upobjects[id]) {	// If no update-request ongoing
				this.upobjects[id] = id;	// save update-request
			this.aUpdateObject(id);
			}
		}
	
		async aUpdateObject(id) {
			$.ajax({
	  		type: 'post',
	  		url: 'scripts/cellData.php',
	  		dataType:'json',
	  		data: {id:id,user:this.data},
	  		success : (json) => {
				// Update Data, then Models and then UI
			 },
	 		complete: (json) => {
	  		 	setTimeout(() => { this.updateObject(id)},,900000);
	  		},
		 		error: function(json) {
					console.log('error');
				}
			});
		}
	
		initUpdater() {
		let count = 1;
		for(const object of Object.values(this.objects)) {
			setTimeout(() => { this.updateObject(object.id)},count*2000);
			count++;
		}
		}
    }

File: Central.js

// Import BABYLON
// Import Updater.js

	export default class {
    	constructor() {
        	this.canvas = document.getElementById('renderCanvas');	// Canvas
			this.engine = new BABYLON.Engine(this.canvas, true, { stencil: true });	// prepare engine
		
			this.scene = new BABYLON.Scene(this.engine);	// init scene
			this.createScene();
		}
	
		createScene() {
			this.assetManager = new BABYLON.AssetsManager(this.scene);	// Model Loader
			this.updater = new Updater(this);	// manage object updates

			// this.updater.objects will be filled here and models loading is prepared 

			const central = this;
			window.addEventListener('resize', function() {
				central.engine.resize()
			});

			this.assetManager.onProgress = (remainingCount, totalCount, task) => {
				console.log(remainingCount, totalCount, task.name);
			}

			this.assetManager.onFinish = (tasks) => {	// If all 3d models are loaded
				this.setup();

				this.engine.runRenderLoop(function() {
					central.scene.render();	// simulate scene
				});
			};
		
			this.assetManager.load();	// Load 3d models
		}

		setup() {
			this.updater.initUpdater();
		}
	}

File: index.js

// Import BABYLON
// Import Central.js

if (BABYLON.Engine.isSupported()) {
	window.central = new Central();	// create Application
}

File: updateData.php

<?PHP
ini_set('MAX_EXECUTION_TIME', 1800);
define('CENTRAL_ROOT_DIR','..');
include_once(CENTRAL_ROOT_DIR.'/path/to/funcs.php');  // includes get_babylon_object_data()
include_once(CENTRAL_ROOT_DIR.'/path/to/config.php');  // includes DB config      
dbconnect();  // connect to mysql DB
$conf = get_all_config();  // get app configuration
include_once(CENTRAL_ROOT_DIR.'/path/to/definitions.php');  // includes definitions

function get_babylon_object($user,$id) {
	return json_encode(get_babylon_object_data($user,$id));	// get_babylon_object_data updates and returns $user (array) using mysqli_fetch_assoc to get DB data
}
echo get_babylon_object($_POST['user'] ?? array(),$_POST['id'] ?? 0);

dbclose();  // close DB
?>

I’d simplify your life a bit and use the engine timings and do something like this, where the fetching happens in update() and the first update is made independent of the initial scene creation (because of the scene.onBeforeRenderObservable.add()):

setup() {  // Warning: untested code!
    let fifteenMinutes = 15*60*60*1000
    let countdown = fifteenMinutes
    scene.onBeforeRenderObservable.add(() => {
        countdown -= engine.getDeltaTime()

        if (countdown < 0) {
           countdown = fifteenMinutes * 10
           update()
           countdown = fifteenMinutes
        }
     })
}
1 Like

I would like to help. Probably you can share the returned data from the PHP code below:

When the JS having problem?

Edit: second thought:

  1. Create global variable, for example window.newData = null;
  2. inside your this.engine.runRenderLoop(function() {, add
    if (window.newData != null) {
    call the update function
    window.newData = null;
    }
  3. inside your async aUpdateObject(id) {, success: just assign the result to window.newData. Do not call the update from there.

Hope this help, regards.

@niu_bee to your #1: I tried return json_encode(array(‘abc’=>5)); but did not work, so the data array is not a problem. To #2: Is it wise to use global arrays in terms of security? Even tho, I tried commenting out all in success, but did not get any better.

Edit: I found the statement that causes the error. It is in code I didn’t post.
In the MoveableObject class, it calculates current position relative to position of StartObject and DestinationObject and (Endtime-currentTime)/wholeMoveTime. This is in scene.registerBeforeRender. And if Start- or DestinationObject do not exist it will call updateObject function to get Object data from DB to create the Object on runtime. Question is how to do it else, this code part in MoveableObject class:

this.scene.registerBeforeRender(function () {
	if(this.data==null) {	// if data of moveable object is gone
		//this.body.dispose();	// delete mesh, DO I NEED this and do i have to dispose childrens too?
		delete this; // delete object
	}
	else {
		if(this.data['endtime']==0) {	 // If stationed
			if(this.updater.objects[this.data['stationid']]!=null) { // If Object to station MoveableObject exist
				this.start = null;
				this.end = null;
				this.station = this.updater.objects[this.data['stationid']];
				this.body.parent = this.station.body;
			}
			else {
				this.updater.updateObject(this.data['stationid']);
			}
		}
		else {	 // If not stationed
			if(this.station) { // If still stationed on an Object
				this.station = null;
				this.body.parent = null; // Do I need this both?
			}
			if(!this.start) {
				if(this.updater.objects[this.data['startid']]!=null) { // If Object to start from MoveableObject exist
					this.start = this.updater.objects[this.data['startid']];
				}
				else {
					this.updater.updateObject(this.data['startid']);
				}
			}
			if(!this.end) {
				if(this.updater.objects[this.data['endid']]!=null) { // If destinated Object of MoveableObject exist
					this.end = this.updater.objects[this.data['end']];
				}
				else {
					this.updater.updateObject(this.data['endid']);
				}
			}
		}
		if(this.start && this.end) {
			// set this.body.position and rotation by time from start to end that is given in data as integers and start & destination object positions
		}
	}
});

Edit: Now I split that into 2 functions and use delay for updates:

navigate() {
	if(this.data==null) {	// if data of moveable object is gone
		//this.body.dispose();	// delete mesh, DO I NEED this and do i have to dispose childrens too?
		delete this; // delete object
	}
	else {
		if(this.data['endtime']==0) {	 // If stationed
			if(this.updater.objects[this.data['stationid']]!=null) { // If Object to station MoveableObject exist
				this.start = null;
				this.end = null;
				this.station = this.updater.objects[this.data['stationid']];
				this.body.parent = this.station.body;
			}
			else {
				setTimeout(() => { this.updater.updateObject(this.data['stationid']); },100000); 
			}
		}
		else {	 // If not stationed
			if(this.station) { // If still stationed on an Object
				this.station = null;
				this.body.parent = null; // Do I need this both?
			}
			if(!this.start) {
				if(this.updater.objects[this.data['startid']]!=null) { // If Object to start from MoveableObject exist
					this.start = this.updater.objects[this.data['startid']];
				}
				else {
					setTimeout(() => { this.updater.updateObject(this.data['startid']); },100000); 
				}
			}
			if(!this.end) {
				if(this.updater.objects[this.data['endid']]!=null) { // If destinated Object of MoveableObject exist
					this.end = this.updater.objects[this.data['end']];
				}
				else {
					setTimeout(() => { this.updater.updateObject(this.data['endid']); },100000); 
				}
			}
		}
	}

move() {
	this.scene.registerBeforeRender(function () {
		if(this.start && this.end) {
			const wTime = this.data['endtime']-this.data['starttime']; // whole move time
			const moved = (this.data['endtime']-Date.now/1000) / (wTime);
			const mVector = this.end.body.position.subtract(this.start.body.position); // move vector
			 this.body.position = mVector.scale(moved); // set position of moveable relative to current time
			this.body.lookAt(this.end.body.position); // set rotation, ANY BETTER WAY TO GET RADIAN HERE?

			this.speed = mVector.scale(1 / (wTime * 60 / this.scene.getAnimationRatio() ) );	// speed = distance / time
			this.body.position.x += this.speed.x;
			this.body.position.y += this.speed.y;
			this.body.position.z += this.speed.z;
		}
	});
}

Edit2: I am now at the point where I got nearer, because the error only occurs when I don’t comment out this line inside get_babylon_object_data():

			$numcflinfpres = dbquery('SELECT fleet_id,fleet_user_id,fleet_cell_id,fleet_planet_id FROM '.$db_table['fleet'].' WHERE fleet_cell_id='.$radarcellsarr['cell_id']);
			while($numcflinfparr = mysqli_fetch_assoc($numcflinfpres)) {
				$numcflinfparr['fleet_ships'] = array();
				$numcflsinfpres = dbquery('SELECT fs_ship_id,fs_ship_cnt FROM '.$db_table['fleet_ships'].' WHERE fs_fleet_id='.$numcflinfparr['fleet_id']);
				while($numcflsarr = mysqli_fetch_assoc($numcflsinfpres)) {
					//array_push($numcflinfparr['fleet_ships'],$numcflsarr);
				}
				array_push($radarcellsarr['cell_fleets'],$numcflinfparr);
			}

So the to be returned array is too big? is there a way too allow bigger arrays, concerning depth?

About the global variable, it just a bad practice. Sorry about that, my aim is to test, then later you can update the script to not using global variable.

I believe the concept is: modify / refresh your screen at every “tick” provided by BJS, it’s about every 1/60 second. The “tick” can be accessed from registerBeforeRender or runRenderLoop.

So, I propose 2 options, the first is update all during “tick”, the second one is to update one each “tick”, use any option that working:

Option 1:
scene.registerBeforeRender(updateAll);
or
engine.runRenderLoop(function () {
  updateAll();
  scene.render();
}

Option 2:
var newData = [];
scene.registerBeforeRender(updateOne);
or
engine.runRenderLoop(function () {
  updateOne();
  scene.render();
}
function updateOne() {
  if (newData.length > 0) {
    let oneData = newData.pop();
    update(oneData); //call the real update function
  }
}

I think, this is what I can do so far, unless you can put everything on PG, so I can clearly read the codes.

Note: one thing that also concern me in your code is this part:

for(const objectof Object.values(this.objects)) {
setTimeout(this.updateObject.bind(this),900000+count*1000,object.id);
count++;
}

I don’t think it’s a good idea to have so many setTimeout.

This part also concerning me:

Also, tried only calling one object update (replacing for…of for a certain objectID), but I had no success. The error occurs on the line of $.ajax statement. Tell me if you need more information.

Even adding a counter to have 1 second between two $.ajax calls, does not work.

if you call your server too often, it’s possible the ajax call is rejected by the server. To avoid this, you can implement “long-polling” or SSE (Server-Sent Events).

I got it working, but needed a workaround, i changed:

			$numcflsinfpres = dbquery('SELECT fs_ship_id,fs_ship_cnt FROM '.$db_table['fleet_ships'].' WHERE fs_fleet_id='.$numcflinfparr['fleet_id']);
			while($numcflsarr = mysqli_fetch_assoc($numcflsinfpres)) {
				array_push($numcflinfparr['fleet_ships'],$numcflsarr);
			}

to a function and used:

array_push($numcflinfparr[‘fleet_ships’], get_babylon_sub_object_data($numcflinfparr[‘fleet_id’]));

@niu_bee

The question is what is recommended, because i update the whole solar system data, which is actually not much just a 4-5 depth associated php array that is built by mysqli_fetch_assocs, but when you seperate parts into function it works, like I showed above?
The problem is I have to update the whole solar system, because otherwise i can’t determine if planets are fusioned when I only update planetwise. I would have to change my whole planet fusion system in html/php/DB and to get planetwise updates working, which will be less mysqli_fetch_assocs into each other, but I hoped not to have to.
Another option could be a extra planet fusion check algorithm by given planetID, which seems the best to me.
Knowing all that, you think I can keep using setTimeout → completed:setTimeout? So update process is initialized once and request itself, when the request for a solar system is completed. I have set timeout to 2 seconds now, so there is 2 seconds between every ajax.