Waiting for several asynchronous things
A report is captured with a screenshot, not converted. By default the engine prints the page once it has loaded, the network is idle and fonts are ready. Anything that draws after that — a chart, a map, a table built from a second fetch — is not on the page yet when the picture is taken.
The template says when it is really finished. For one piece of drawing, paragon.ready() is
enough. For several, use the barrier: register each piece, and the engine waits for the last one.
Nothing needs to be loaded or imported. window.paragon is injected into every template before
your scripts run.
This object used to be called reportEngine, and that name still resolves to the same object, so a
template written before the rename keeps working untouched. Write new templates against paragon.
The API
| Call | Use it for |
|---|---|
paragon.waitFor(promise) |
Anything that gives you a promise. Returns the promise, so it chains |
paragon.pending() |
Callback-style code with no promise. Returns a release function to call when that piece is done |
paragon.ready() |
Opens the gate immediately, whatever else is outstanding |
The page is captured once every registered piece has finished. ready() is the escape hatch, not
the normal path — with a barrier in play it cuts off work that is still running.
Set Template draws asynchronously on the report ("awaitsReadySignal": true in the manifest)
whenever this code lives in an asset file. The engine can spot a readiness call written directly into
the template, but it cannot see inside a .js — and chart code almost always lives in one.
Several charts
Each chart registers itself as it starts. Order does not matter and no chart needs to know about the others, which is what makes this work across separate asset files.
// assets/js/report-charts.js
// Chart libraries are event-based, so turn "drawn" into a promise once and reuse it.
// Use whichever event yours raises when a series has finished rendering.
const drawn = (series) =>
new Promise(resolve => series.events.once("rendered", resolve));
paragon.waitFor(drawn(revenueSeries));
paragon.waitFor(drawn(marginSeries));
paragon.waitFor(drawn(headcountSeries));
Data first, then a chart from it
Work started inside a tracked promise is registered before the earlier one is released, so the barrier stays closed across the whole chain.
paragon.waitFor(
fetch("assets/data/regions.json")
.then(response => response.json())
.then(regions => {
const chart = buildRegionChart(regions);
return drawn(chart.series);
})
);
An await chain reads the same way — the barrier holds until the whole function settles:
paragon.waitFor((async () => {
const regions = await loadRegions();
const chart = buildRegionChart(regions);
await drawn(chart.series);
})());
Callback code with no promise
pending() hands back a release function. Call it when that piece is finished; calling it twice is
harmless.
const release = paragon.pending();
map.on("tilesloaded", () => {
labelTheMap();
release();
});
Images the browser has not decoded yet
Loading is not decoding — a large image can still be blank when the page is otherwise idle.
document.querySelectorAll("img").forEach(image => {
paragon.waitFor(image.decode().catch(() => {}));
});
Web fonts a script loads later
Fonts in the template's CSS are already waited for. One requested from script is not.
const face = new FontFace("Paragon Numerals", "url(assets/fonts/numerals.woff2)");
paragon.waitFor(
face.load().then(loaded => document.fonts.add(loaded))
);
Failures
A rejected promise releases the barrier rather than holding it. The error is written to the console and the report is produced with that piece missing.
This is deliberate. A report with one empty chart shows you which chart broke; a render that hangs
until RenderTimeout and throws ReportRenderTimeoutException tells you only that something,
somewhere, never finished.
If a piece failing should fail the whole report, do that in your own code — let the promise reject after you have logged what you need, and check the output.
Traps
Register synchronously, or from inside tracked work. These two are safe:
paragon.waitFor(a); // registered now
paragon.waitFor(b.then(() => c)); // registered inside tracked work
This one is not:
setTimeout(() => paragon.waitFor(c), 50); // may arrive after the gate has opened
The barrier allows one tick after the last piece finishes for follow-on work to appear. A timer longer than that can miss the window. Wrap the delay itself instead:
paragon.waitFor(new Promise(resolve => setTimeout(resolve, 50)).then(() => c));
Do not mix ready() into a barrier. ready() opens the gate immediately. A chart that calls it
on completion, in a report where three charts are registered, captures the page when the fastest
chart finishes.
A promise that never settles hangs the render. No resolve, no reject, no capture — you get
ReportRenderTimeoutException after RenderTimeout. Give anything that might not answer its own
deadline:
const withDeadline = (work, ms) => Promise.race([
work,
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out")), ms))
]);
paragon.waitFor(withDeadline(slowThing(), 5000));
Animations are already off. The engine disables every CSS animation and transition, so nothing needs to be waited out. Turn your chart library's own animation off as well — most have a single setting for it — because an animating chart reports itself as drawn while it is still moving.
Checking it
Preview the report in the designer. If it comes out with a blank chart, the barrier is opening too early; if it times out, something registered never settled — the browser console names it.
See also Report template format — schema 2.0 for the manifest, and the Template draws asynchronously toggle on the report itself.