Skip to content

feat(layout-elk): add layoutSync() and settle main-thread layout() before paint - #3589

Open
Geliogabalus wants to merge 1 commit into
clientIO:devfrom
Geliogabalus:feat/layout-elk-sync-layout
Open

Geliogabalus wants to merge 1 commit into
clientIO:devfrom
Geliogabalus:feat/layout-elk-sync-layout

Conversation

@Geliogabalus

Copy link
Copy Markdown
Contributor

Description

  • layout() without an elk option now settles before the next paint once ELK is loaded.
  • New layoutSync(graphOrCells, opt?) lays out synchronously and returns the LayoutResult. It takes layout()'s options except elk and signal, and throws if ELK isn't loaded yet, before any export callback runs.
  • New loadElk() loads the main-thread ELK ahead of time. It reuses the existing dynamic chunk, so nothing new is imported statically.

How

elkjs (0.12.0) only reaches its layout handler after two setTimeout(…, 0): one in its fake worker (FakeWorker.postMessage) and one in its promise wrapper (PromisedWorker's onmessage). ELK itself computes synchronously.

The default ELK instance already holds its fake worker at elk.worker.worker. That worker's dispatcher.dispatch() runs a command synchronously, lays the graph out in place and throws on failure. defaultElk.mts calls it directly. No worker is created, and elk-worker.js is not imported.

Because this relies on elkjs internals (elkjs is pinned), the path is guarded:

  • If the dispatcher is missing, layout() falls back to elk.layout() and layoutSync() throws a clear error.
  • The algorithms are registered synchronously on first use. The ELK constructor's own register is still waiting on a setTimeout at that point.
  • __java$exception is stripped from thrown errors, as elkjs' wrapper does on the async path.
  • Messages use a string id, so ELK's answer, which still goes through the wrapper, can never settle one of the wrapper's numbered promises.

layout() is split into prepare and apply halves that layoutSync() shares. README and changesets are updated.

Motivation and Context

On the main thread, layout() always settled after the browser had painted at least one frame. Anything laid out right after a graph change (a cell added, a label edited) was first drawn at its pre-layout position, then jumped. @joint/layout-directed-graph doesn't have this problem because it is synchronous.

With this change:

  • The default layout() no longer jumps; existing callers need no changes.
  • layoutSync() covers callers that need the layout applied before anything else runs. One example is @joint/react's useOnElementsMeasured, which flushes the paper as soon as its callback returns.

Synchronous means blocking. That suits small and medium graphs; large ones should still use createWorkerElk().

Tests

  • layoutSync() throws before ELK is loaded and after a failed load, without running export callbacks.
  • layout() settles before a setTimeout(…, 0) fires.
  • Errors are flattened the way elkjs flattens them.
  • layoutSync() gives the same positions and vertices as layout(), applies in a single layout batch, and handles cell lists and callbacks.
  • A later layout() with elkjs' own ELK instance is unaffected.
  • Type tests for the new exports and the options layoutSync() rejects.

Not covered: the fallback when the dispatcher is missing. The shared instance is cached once it loads, so the test bundle can't replace it.

yarn workspace @joint/layout-elk run test passes: 113 tests, coverage floors met.

🤖 Generated with Claude Code

…fore paint

elkjs only reaches its layout handler after two setTimeout(…, 0) - one in
its fake worker, one in its promise wrapper - so a main-thread layout()
always settled after the browser painted the graph as it was before.

Call the handler of the default ELK instance's own fake worker directly
(an elkjs internal, guarded, with a fallback to elk.layout()). layout()
then settles in microtasks once ELK is loaded, and the new layoutSync()
lays out synchronously; loadElk() loads ELK ahead of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant