RenderQueue.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2026, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.teavm.js.browser;
import java.util.ArrayList;
import java.util.List;
import org.teavm.jso.JSBody;
/**
* The render requests of the JavaScript engines, answered one at a time, in
* the order received, without any thread.
*
* <p>
* A request is rendered by attempts (see {@link PendingResources}): an attempt
* that missed resources is discarded, the resources are loaded with JavaScript
* callbacks, and the request is rendered again, up to
* {@link PendingResources#MAX_ROUNDS} times. Only the attempt that is kept is
* delivered: a {@link Job} writes nothing (DOM, callbacks) before
* {@link Job#deliver()}.
*
* <p>
* Rules:
* <ul>
* <li>First in, first out. While the head request waits for its resources, the
* others wait behind it.</li>
* <li>A job may replace a waiting one ({@link Job#replaces(Job)}), in place:
* this is the "latest wins" of <code>render</code>. A job waiting for its
* resources can be replaced as well; its loads are then wasted.</li>
* <li>Re-entrance: a request is never rendered from the call that makes it,
* but from a later JavaScript task. A request made while the queue is working
* (from a callback of a delivered request, typically) is queued, and answered
* after the ones already waiting, never in the middle of another
* rendering.</li>
* <li>Nothing thrown by a job stops the queue.</li>
* </ul>
*/
public final class RenderQueue {
// ::remove file when JAVA8
/** One request. */
public static abstract class Job {
private int rounds;
/**
* Renders the request once. It must keep its result for {@link #deliver()}
* and change nothing outside: it may be discarded.
*/
protected abstract void attempt();
/** Delivers the result of the last attempt. Called once. */
protected abstract void deliver();
/**
* Delivers a failure instead of the result: resources were still missing
* after {@link PendingResources#MAX_ROUNDS} rounds. Called once, instead of
* {@link #deliver()}.
*/
protected abstract void giveUp(String message);
/** Whether this job replaces <code>waiting</code>, a job not answered yet. */
protected boolean replaces(Job waiting) {
return false;
}
}
/** The requests not answered yet, oldest first. */
private static final List<Job> QUEUE = new ArrayList<Job>();
/** Whether {@link #pump()} is on the stack. */
private static boolean running;
/** Whether the head of the queue waits for its resources. */
private static boolean loading;
/** Whether {@link #pump()} is already scheduled. */
private static boolean scheduled;
private RenderQueue() {
}
/**
* Queues a request. It is answered from a later JavaScript task, never from
* this call: the caller gets its answer after it has returned, as when a
* worker thread answered, and several calls made in the same task are all
* queued before the first one is rendered (which is what lets a later
* {@code render} replace an earlier one).
*/
public static void submit(Job job) {
boolean replaced = false;
for (int i = 0; i < QUEUE.size() && replaced == false; i++)
if (job.replaces(QUEUE.get(i))) {
QUEUE.set(i, job);
replaced = true;
}
if (replaced == false)
QUEUE.add(job);
schedule();
}
private static void schedule() {
if (scheduled || running || loading)
return;
scheduled = true;
defer(() -> {
scheduled = false;
pump();
});
}
private static void pump() {
if (running || loading)
return;
running = true;
try {
while (QUEUE.isEmpty() == false) {
final Job job = QUEUE.get(0);
if (job.rounds == 0)
PendingResources.startRequest();
PendingResources.startAttempt();
try {
job.attempt();
} catch (Throwable t) {
// Jobs catch their own failures; this is a last resort, which must not
// leave the queue stuck.
consoleError("PlantUML: request failed: " + t);
}
final boolean missing = PendingResources.hasMissing();
if (missing && job.rounds < PendingResources.MAX_ROUNDS) {
PendingResources.endAttempt(false);
job.rounds++;
loading = true;
PendingResources.loadMissing(() -> {
loading = false;
pump();
});
return;
}
final String unresolved = missing ? PendingResources.describeMissing() : null;
PendingResources.endAttempt(true);
QUEUE.remove(0);
try {
if (unresolved == null)
job.deliver();
else
job.giveUp("Rendering abandoned after " + PendingResources.MAX_ROUNDS
+ " rounds of loading resources; still missing: " + unresolved);
} catch (Throwable t) {
consoleError("PlantUML: request failed: " + t);
}
}
} finally {
running = false;
}
}
@JSBody(params = "callback", script = "setTimeout(function() { callback(); }, 0);")
private static native void defer(ScriptLoaderJs.Ok callback);
/** Reports an error to the console, whatever {@code PLANTUML_DEBUG} says. */
@JSBody(params = "msg", script = "console.error(msg);")
static native void consoleError(String msg);
}