[JS, Jitter] Unexpected behavior when executing many async processes with jit.gl objects in v8

weuoimi 's icon

Hello, i've been working on a patch with 3d-animated graph flood-fill recursive algorithm, the main feature of which is that each transition between two vertices is asynchronous and time-consuming. The algo resembles tree traversing algorithm in some way. During testing, i noticed that sometimes the animations are either skipped or started at the wrong time in wrong vertices. I've tested the algorithm in vanilla js and it worked fine, but trying to tie up jit.gl object creation and using jit.gl.node for animation of the gridshapes leads to the strange behaviour. I do not exclude the possibility that I just wrote the algorithm badly, but I tried many different variations and in all cases the problems were the same, and even in the same vertices.

That's why i want to know about some intricacies of using JitterObject and Tasks inside of async functions, i suppose that causes the issue. Here is a problematic code, fill() is the algorithm itself:

class FloodFill {

    constructor(graph, animationSpeed) {
        this.graph = graph;
        this.animationSpeed = animationSpeed;
    }

    async animateImpulse(startCoord, endCoord, speed) {
        return new Promise((resolve) => {
            const gridshape = new JitterObject("jit.gl.gridshape", contextName);
            gridshape.shape = "cone";
            gridshape.color = [Math.random(), Math.random(), Math.random()];

            const animNode = new JitterObject("jit.anim.node");
            gridshape.anim = animNode.name;
            animNode.movemode = "local";
            animNode.scale = impulseScale;
            animNode.position = startCoord;
            animNode.lookat = endCoord;

            const animDrive = new JitterObject("jit.anim.drive");
            animDrive.targetname = animNode.name;
            animDrive.move(0, 0, speed);

            const goalVector = endCoord.map((v, i) => v - startCoord[i]);

            const checkPosition = new Task(() => {
                const pos = animNode.worldpos;
                const currentVector = pos.map((v, i) => v - startCoord[i]);

                if (veclength(currentVector) >= veclength(goalVector)) {
                    // post("Animation complete\n");
                    gridshape.freepeer();
                    animNode.freepeer();
                    animDrive.freepeer(); 
                    checkPosition.cancel();
                    resolve();  
                    // post("Resolved promise\n");
                }
            });

            checkPosition.interval = worldposQueryInterval;
            checkPosition.repeat();
        })
    }

    async fill(startVertexId, stepsLeft, actionCallback) {
        if (stepsLeft <= 0) {
            post("Stopped at vertex: ", startVertexId, ", no steps left\n");
            return;
        }

        const connections = this.graph[startVertexId]?.connections || [];

        if (connections.length === 0) {
            post("Stopped at vertex: ", startVertexId, ", no connections\n");
            return;
        }

        const powerPerConnection = Math.floor(stepsLeft / connections.length);
        post("Power per connection: ", powerPerConnection, "\n");

        if (powerPerConnection <= 1) {
            post("Power per connection too low, stopping recursion\n");
            return;
        }

        try {
            await Promise.all(connections.map(async (nextVertexId) => {
                try {
                    await this.animateImpulse(
                        this.graph[startVertexId]?.coordinates,
                        this.graph[nextVertexId]?.coordinates,
                        animationSpeed * scaleToAnimationSpeed(this.graph[startVertexId], this.graph[nextVertexId])
                    );
                    // if the "bang" attribute is set and true, actionCallback is called
                    if (this.graph[nextVertexId]?.bang) {
                        actionCallback(nextVertexId, this.graph[nextVertexId]?.cell);
                    }
                } catch (e) {
                    post("Error in animateImpulse: ", e, "\n");
                }
 
                await this.fill(nextVertexId, powerPerConnection - 1, actionCallback);
            }));
        } catch (e) { 
            post("Error resolving Promise in fill: ", e, "\n");
        }
    }
}
graph.maxpat
Max Patch