@zombie-mermaid/svg-renderer 4.2.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/index.cjs +52 -52
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +23 -4
  5. package/dist/index.d.ts +23 -4
  6. package/dist/index.js +897 -849
  7. package/dist/index.js.map +1 -1
  8. package/package.json +14 -5
  9. package/src/__tests__/c4-label-clearance-1290.test.ts +0 -112
  10. package/src/__tests__/c4-line-ends-1208.test.ts +0 -52
  11. package/src/__tests__/c4-mermaid-reference.test.ts +0 -124
  12. package/src/__tests__/c4-svg.test.ts +0 -337
  13. package/src/__tests__/c4-text-width-1210.test.ts +0 -43
  14. package/src/__tests__/class-component-order-1249.test.ts +0 -40
  15. package/src/__tests__/class-diamond-markers-1232.test.ts +0 -59
  16. package/src/__tests__/class-member-right-padding-1238.test.ts +0 -43
  17. package/src/__tests__/class-namespace-1196.test.ts +0 -165
  18. package/src/__tests__/class-namespace-sparse-elk-1196.test.ts +0 -96
  19. package/src/__tests__/class-visibility-markers-1237.test.ts +0 -27
  20. package/src/__tests__/edge-jog-straightening-1388.test.ts +0 -57
  21. package/src/__tests__/elk-adapter-utils.test.ts +0 -166
  22. package/src/__tests__/er-attribute-right-padding-1262.test.ts +0 -47
  23. package/src/__tests__/er-cardinality-markers-1233.test.ts +0 -35
  24. package/src/__tests__/er-default-direction-1250.test.ts +0 -69
  25. package/src/__tests__/er-edge-label-contrast-1244.test.ts +0 -58
  26. package/src/__tests__/fixtures/c4-mermaid-reference.json +0 -626
  27. package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +0 -168
  28. package/src/__tests__/pie-layout.test.ts +0 -215
  29. package/src/__tests__/pie-svg.test.ts +0 -194
  30. package/src/__tests__/render-mermaid-svg.test.ts +0 -64
  31. package/src/__tests__/sequence-label-clearance-1242.test.ts +0 -162
  32. package/src/__tests__/sequence-nested-activation-1241.test.ts +0 -37
  33. package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +0 -32
  34. package/src/__tests__/xychart-smooth-curve.test.ts +0 -42
  35. package/src/__tests__/xychart-x-label-overlap-1243.test.ts +0 -62
  36. package/src/c4/arial-widths.ts +0 -59
  37. package/src/c4/layout.ts +0 -680
  38. package/src/c4/metrics.ts +0 -261
  39. package/src/c4/renderer.ts +0 -366
  40. package/src/class/layout.ts +0 -525
  41. package/src/class/renderer.ts +0 -669
  42. package/src/edge-curves.ts +0 -205
  43. package/src/elk-instance.ts +0 -292
  44. package/src/er/layout.ts +0 -216
  45. package/src/er/renderer.ts +0 -516
  46. package/src/generated/mono-font-subset.ts +0 -32
  47. package/src/index.ts +0 -277
  48. package/src/layout-engine/back-edges.ts +0 -73
  49. package/src/layout-engine/compound-flat.ts +0 -916
  50. package/src/layout-engine/constants.ts +0 -19
  51. package/src/layout-engine/edge-bundling.ts +0 -458
  52. package/src/layout-engine/elk-adapter-utils.ts +0 -81
  53. package/src/layout-engine/elk-graph-builder.ts +0 -245
  54. package/src/layout-engine/from-elk.ts +0 -767
  55. package/src/layout-engine/inner-edges.ts +0 -329
  56. package/src/layout-engine/jog-straightening.ts +0 -52
  57. package/src/layout-engine/layer-alignment.ts +0 -174
  58. package/src/layout-engine/layout-hints.ts +0 -27
  59. package/src/layout-engine/to-elk.ts +0 -855
  60. package/src/layout-engine.ts +0 -115
  61. package/src/pie/layout.ts +0 -234
  62. package/src/pie/renderer.ts +0 -161
  63. package/src/registry.ts +0 -441
  64. package/src/renderer.ts +0 -1510
  65. package/src/resolve-colors.ts +0 -339
  66. package/src/sequence/layout.ts +0 -766
  67. package/src/sequence/renderer.ts +0 -687
  68. package/src/shape-clipping.ts +0 -197
  69. package/src/styles.ts +0 -145
  70. package/src/title-gaps.ts +0 -161
  71. package/src/xychart/layout.ts +0 -727
  72. package/src/xychart/renderer.ts +0 -676
@@ -1,205 +0,0 @@
1
- // ============================================================================
2
- // zombie-mermaid — edge path interpolation (Mermaid's `flowchart.curve`)
3
- //
4
- // ELK routes an edge as a list of bend points. How those points are joined
5
- // into a drawn line is a separate, purely presentational choice, which
6
- // Mermaid exposes as `%%{init: {"flowchart": {"curve": "basis"}}}%%`.
7
- //
8
- // Every curve here consumes the same routed points, so switching styles never
9
- // changes where an edge goes — only how it looks getting there.
10
- // ============================================================================
11
-
12
- import type { Point, CurveStyle } from '@zombie-mermaid/core'
13
- import { f } from '@zombie-mermaid/core'
14
-
15
- /** `M x y` for the first point. */
16
- function moveTo(p: Point): string {
17
- return f`M ${p.x} ${p.y}`
18
- }
19
-
20
- /** Straight segments through every point — the historical default. */
21
- function linearPath(points: Point[]): string {
22
- return (
23
- moveTo(points[0]!) +
24
- points
25
- .slice(1)
26
- .map((p) => f` L ${p.x} ${p.y}`)
27
- .join('')
28
- )
29
- }
30
-
31
- /**
32
- * Uniform cubic B-spline, matching d3's `curveBasis` (what Mermaid uses for
33
- * `basis`).
34
- *
35
- * A B-spline is *approximating*, not interpolating: it passes through neither
36
- * the interior points nor, without the endpoint handling below, the ends. The
37
- * first and last points are therefore emitted explicitly so an edge still
38
- * touches the nodes it connects — otherwise it would visibly detach.
39
- *
40
- * This is a direct port of d3's `Basis` curve rather than a lookalike, so a
41
- * diagram rendered here traces the same path Mermaid would draw. For each
42
- * point d3 emits one cubic whose controls are the thirds between the two
43
- * preceding points and whose end is the B-spline knot `(p0 + 4p1 + p2) / 6`;
44
- * it leads in at `(5p0 + p1) / 6` and closes with a final cubic against a
45
- * repeated last point, then a line to it.
46
- */
47
- function basisPath(points: Point[]): string {
48
- if (points.length < 3) return linearPath(points)
49
-
50
- const last = points[points.length - 1]!
51
- const parts: string[] = [moveTo(points[0]!)]
52
-
53
- /** One B-spline segment across three successive control points. */
54
- const segment = (p0: Point, p1: Point, p2: Point): string =>
55
- f` C ${(2 * p0.x + p1.x) / 3} ${(2 * p0.y + p1.y) / 3},` +
56
- f` ${(p0.x + 2 * p1.x) / 3} ${(p0.y + 2 * p1.y) / 3},` +
57
- f` ${(p0.x + 4 * p1.x + p2.x) / 6} ${(p0.y + 4 * p1.y + p2.y) / 6}`
58
-
59
- // Lead-in to the first knot, one sixth of the way along the opening leg.
60
- parts.push(
61
- f` L ${(5 * points[0]!.x + points[1]!.x) / 6}` +
62
- f` ${(5 * points[0]!.y + points[1]!.y) / 6}`,
63
- )
64
-
65
- for (let i = 2; i < points.length; i++) {
66
- parts.push(segment(points[i - 2]!, points[i - 1]!, points[i]!))
67
- }
68
-
69
- // d3 closes by feeding the last point twice, then drawing to it.
70
- parts.push(segment(points[points.length - 2]!, last, last))
71
- parts.push(f` L ${last.x} ${last.y}`)
72
-
73
- return parts.join('')
74
- }
75
-
76
- /**
77
- * Straight runs joined by rounded corners. Mermaid calls this `natural`.
78
- *
79
- * NOTE: this is deliberately *not* d3's interpolating natural cubic spline.
80
- * ELK routes edges orthogonally, so every bend is a right angle, and at a
81
- * right angle the tangent of any C1-smooth interpolating spline is the
82
- * average of the two segment directions — i.e. diagonal. The curve must then
83
- * arrive at the corner travelling diagonally while approaching from directly
84
- * above, so it bulges sideways past the straight line. Two mirrored edges
85
- * leaving one decision node turned that bulge into a visible teardrop loop.
86
- * Scaling the tangents down shrinks the overshoot but cannot remove it; it is
87
- * a property of interpolating a 90° corner smoothly.
88
- *
89
- * Rounding the corners instead gives a smooth path that never overshoots and
90
- * stays visually distinct from `basis`, which cuts corners far more deeply.
91
- * The trade is that the path passes *near* each bend rather than exactly
92
- * through it — documented in docs/diagrams.md.
93
- */
94
- function naturalPath(points: Point[]): string {
95
- if (points.length < 3) return linearPath(points)
96
-
97
- /** Largest corner radius, in px. Kept small so edges stay readable. */
98
- const MAX_RADIUS = 12
99
-
100
- const parts: string[] = [moveTo(points[0]!)]
101
-
102
- for (let i = 1; i < points.length - 1; i++) {
103
- const prev = points[i - 1]!
104
- const corner = points[i]!
105
- const next = points[i + 1]!
106
-
107
- const inLen = Math.hypot(corner.x - prev.x, corner.y - prev.y)
108
- const outLen = Math.hypot(next.x - corner.x, next.y - corner.y)
109
-
110
- // A zero-length leg means duplicated routed points; there is no corner
111
- // to round, so pass straight through.
112
- if (inLen === 0 || outLen === 0) {
113
- parts.push(f` L ${corner.x} ${corner.y}`)
114
- continue
115
- }
116
-
117
- // Never consume more than half of either adjacent leg, so neighbouring
118
- // corners cannot overlap and invert the path.
119
- const radius = Math.min(MAX_RADIUS, inLen / 2, outLen / 2)
120
-
121
- const startX = corner.x - ((corner.x - prev.x) / inLen) * radius
122
- const startY = corner.y - ((corner.y - prev.y) / inLen) * radius
123
- const endX = corner.x + ((next.x - corner.x) / outLen) * radius
124
- const endY = corner.y + ((next.y - corner.y) / outLen) * radius
125
-
126
- // Straight up to the fillet, then a quadratic through the corner.
127
- parts.push(f` L ${startX} ${startY}`)
128
- parts.push(f` Q ${corner.x} ${corner.y}, ${endX} ${endY}`)
129
- }
130
-
131
- const last = points[points.length - 1]!
132
- parts.push(f` L ${last.x} ${last.y}`)
133
-
134
- return parts.join('')
135
- }
136
-
137
- /**
138
- * Right-angle staircase between consecutive points.
139
- *
140
- * `where` picks when the step happens: at the midpoint (`step`), immediately
141
- * (`stepBefore`), or at the end of the run (`stepAfter`) — matching d3's
142
- * curveStep family.
143
- *
144
- * The final segment is deliberately left straight, running along the original
145
- * approach direction. An SVG marker with `orient="auto"` takes its angle from
146
- * the last path segment, so a staircase that ends on a horizontal leg points
147
- * the arrowhead sideways into the target node instead of at it. Keeping the
148
- * last leg means the arrow still aims the way the routed edge does.
149
- */
150
- function stepPath(points: Point[], where: 'mid' | 'before' | 'after'): string {
151
- const parts: string[] = [moveTo(points[0]!)]
152
- const lastIndex = points.length - 1
153
-
154
- for (let i = 0; i < lastIndex; i++) {
155
- const a = points[i]!
156
- const b = points[i + 1]!
157
-
158
- if (i === lastIndex - 1) {
159
- parts.push(f` L ${b.x} ${b.y}`)
160
- break
161
- }
162
-
163
- if (where === 'before') {
164
- parts.push(f` L ${a.x} ${b.y} L ${b.x} ${b.y}`)
165
- } else if (where === 'after') {
166
- parts.push(f` L ${b.x} ${a.y} L ${b.x} ${b.y}`)
167
- } else {
168
- const midY = (a.y + b.y) / 2
169
- parts.push(f` L ${a.x} ${midY} L ${b.x} ${midY} L ${b.x} ${b.y}`)
170
- }
171
- }
172
-
173
- return parts.join('')
174
- }
175
-
176
- /**
177
- * Build the `d` attribute for an edge's routed points under `curve`.
178
- *
179
- * A path with fewer than two points cannot be drawn; the caller already skips
180
- * those, but this degrades to an empty string rather than emitting `M`
181
- * followed by nothing.
182
- */
183
- export function pointsToPath(
184
- points: Point[],
185
- curve: CurveStyle = 'linear',
186
- ): string {
187
- if (points.length === 0) return ''
188
- if (points.length === 1) return moveTo(points[0]!)
189
-
190
- switch (curve) {
191
- case 'basis':
192
- return basisPath(points)
193
- case 'natural':
194
- return naturalPath(points)
195
- case 'step':
196
- return stepPath(points, 'mid')
197
- case 'stepBefore':
198
- return stepPath(points, 'before')
199
- case 'stepAfter':
200
- return stepPath(points, 'after')
201
- case 'linear':
202
- default:
203
- return linearPath(points)
204
- }
205
- }
@@ -1,292 +0,0 @@
1
- /**
2
- * Shared ELK instance singleton.
3
- *
4
- * Uses elk.bundled.js (pure synchronous JS, ~1.6 MB) for all environments.
5
- * The singleton is created lazily on first use and cached forever.
6
- *
7
- * ELK's FakeWorker wraps both postMessage and onmessage in setTimeout(0),
8
- * making the normal API fully async. To bypass this:
9
- * 1. During construction, we capture setTimeout(0) callbacks and flush them
10
- * synchronously — this registers the layout algorithms immediately.
11
- * 2. For layout calls, we call dispatcher.saveDispatch() directly (skipping
12
- * the FakeWorker's postMessage setTimeout) and intercept the result via
13
- * rawWorker.onmessage (which the dispatcher calls synchronously).
14
- */
15
-
16
- import type { ELK, ElkNode } from 'elkjs'
17
- import ELKBundled from 'elkjs/lib/elk.bundled.js'
18
- import type { LayoutCache } from '@zombie-mermaid/core'
19
-
20
- /** The message envelope ELK's FakeWorker passes to `dispatcher.saveDispatch()`
21
- * to request a layout run. Mirrors the shape elk-worker.min.js expects on
22
- * `data` for a `cmd: 'layout'` request — not part of elkjs's public types. */
23
- interface ElkWorkerRequest {
24
- id: number
25
- cmd: 'layout'
26
- graph: ElkNode
27
- }
28
-
29
- /** The message envelope ELK's dispatcher passes back to `onmessage` once a
30
- * layout run completes. Mirrors elk-worker.min.js's response shape for a
31
- * `cmd: 'layout'` request — not part of elkjs's public types. */
32
- interface ElkWorkerResponse {
33
- id: number
34
- data?: ElkNode
35
- error?: unknown
36
- }
37
-
38
- interface RawFakeWorker {
39
- postMessage(msg: unknown): void
40
- onmessage: ((e: { data: ElkWorkerResponse }) => void) | null
41
- dispatcher: {
42
- saveDispatch(msg: { data: ElkWorkerRequest }): void
43
- }
44
- }
45
-
46
- /**
47
- * The shape of elkjs's bundled `ELK` instance that actually exists at
48
- * runtime, including the internal `worker` handle. elkjs's public `ELK`
49
- * type (from `elk-api.d.ts`) only declares `layout`/`knownLayout*`/
50
- * `terminateWorker` — it deliberately doesn't expose worker internals,
51
- * since those aren't a supported API. We rely on them anyway (see file
52
- * header), so this extends the public type with the internal piece we
53
- * touch instead of casting through `unknown`.
54
- */
55
- interface ElkBundledInternal extends ELK {
56
- worker: { worker: RawFakeWorker }
57
- }
58
-
59
- let elk: ELK | null = null
60
- let rawWorker: RawFakeWorker | null = null
61
-
62
- // ============================================================================
63
- // Opt-in layout cache
64
- // ============================================================================
65
-
66
- /**
67
- * `LayoutCache`'s shape lives in `@zombie-mermaid/core`'s `types.ts`
68
- * because `RenderOptions.layoutCache` references it and `core` must not
69
- * type-import this package (zombie-mermaid#625). Re-exported here so this
70
- * module stays the single import site for everything layout-cache-related,
71
- * exactly as before the split.
72
- */
73
- export type { LayoutCache }
74
-
75
- /**
76
- * Concrete shape of a `LayoutCache`, restated locally.
77
- *
78
- * `core`'s `LayoutCache` interface marks `map`/`maxSize` `@internal`
79
- * (see the comment on that interface in packages/core/src/types.ts), so
80
- * api-extractor strips them from `@zombie-mermaid/core`'s *published*
81
- * `dist/index.d.ts` — deliberately, so they stay invisible to
82
- * `@zombie-mermaid/core` consumers and to `zombie-mermaid`'s own public
83
- * types (which re-export `LayoutCache` from this package). As of #769,
84
- * `@zombie-mermaid/core` is a real, independently-built dependency of this
85
- * package rather than bundled source, so this module now resolves
86
- * `LayoutCache` through that same trimmed public declaration too — same as
87
- * any other consumer. This module is the one place, in or out of `core`,
88
- * that actually builds and mutates those fields (`createLayoutCache()`
89
- * below; the cache read/evict logic in `elkLayoutSync()`), so it restates
90
- * the concrete shape here — kept in sync by hand with `core`'s source of
91
- * truth — rather than either widening `core`'s public surface or fighting
92
- * api-extractor's trimming in the build config to get it back.
93
- */
94
- interface LayoutCacheShape {
95
- map: Map<string, ElkNode>
96
- maxSize: number
97
- }
98
-
99
- const DEFAULT_LAYOUT_CACHE_SIZE = 20
100
-
101
- /**
102
- * Create a new opt-in layout cache with a bounded size (default 20
103
- * entries). Once full, the least-recently-used entry is evicted to make
104
- * room for a new one.
105
- *
106
- * Pass the result to `elkLayoutSync()` directly, or via
107
- * `RenderOptions.layoutCache` (threaded through by `layoutGraphSync()`,
108
- * `layoutClassDiagramSync()`, and `layoutErDiagramSync()`) to memoize
109
- * layout across repeated renders of the same diagram + options.
110
- */
111
- export function createLayoutCache(
112
- maxSize: number = DEFAULT_LAYOUT_CACHE_SIZE,
113
- ): LayoutCache {
114
- if (!Number.isInteger(maxSize) || maxSize < 1) {
115
- throw new Error(
116
- `createLayoutCache: maxSize must be a positive integer, got ${maxSize}`,
117
- )
118
- }
119
- // Typed as the concrete shape first (see `LayoutCacheShape` above), then
120
- // returned as the public, trimmed `LayoutCache` — a plain widening
121
- // assignment, not a cast.
122
- const cache: LayoutCacheShape = { map: new Map(), maxSize }
123
- return cache
124
- }
125
-
126
- /**
127
- * Deterministically serialize a value for use as a cache key, sorting
128
- * object keys recursively so two structurally-equal ELK input graphs
129
- * always produce an identical string regardless of property-insertion
130
- * order. Plain `JSON.stringify()` does not guarantee that — and this
131
- * function is the only thing standing between a cache hit and returning
132
- * some *other* diagram's layout, so it can't be allowed to drift with
133
- * insertion order.
134
- *
135
- * The ELK input graph built by `mermaidToElk()` is plain JSON (arrays,
136
- * plain objects, strings, numbers, booleans — the shape ELK's own JSON
137
- * schema requires) with every render option that affects layout already
138
- * baked in (direction, spacing, per-subgraph overrides, etc.), so
139
- * serializing the graph itself is a complete and correct cache key: equal
140
- * serialized input always means equal ELK output, since ELK layout is a
141
- * pure function of its input graph.
142
- */
143
- function stableStringify(value: unknown): string {
144
- if (Array.isArray(value)) {
145
- return `[${value.map(stableStringify).join(',')}]`
146
- }
147
- if (value !== null && typeof value === 'object') {
148
- const record = value as Record<string, unknown>
149
- const keys = Object.keys(record).sort()
150
- const entries = keys.map(
151
- (key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`,
152
- )
153
- return `{${entries.join(',')}}`
154
- }
155
- return JSON.stringify(value)
156
- }
157
-
158
- /**
159
- * Ensure the ELK singleton exists.
160
- *
161
- * Patches setTimeout during construction to capture and synchronously flush
162
- * the algorithm registration callback that ELK queues via setTimeout(0).
163
- * Without this, layout calls fail with "algorithm not found" until the
164
- * next macrotask.
165
- */
166
- function ensureElk(): RawFakeWorker {
167
- if (elk && rawWorker) return rawWorker
168
-
169
- // Capture setTimeout(0) callbacks queued during ELK construction
170
- const pending: (() => void)[] = []
171
- const origSetTimeout = globalThis.setTimeout
172
- // @ts-expect-error — simplified signature for our interception, not
173
- // assignment-compatible with the full `typeof setTimeout` overload set
174
- globalThis.setTimeout = (fn: () => void, delay?: number) => {
175
- if (delay === 0) {
176
- pending.push(fn)
177
- return 0
178
- }
179
- return origSetTimeout(fn, delay)
180
- }
181
-
182
- // Bun defines `self` (= globalThis) but not `document`, which tricks
183
- // elk-worker.min.js into taking the Web Worker branch instead of the
184
- // CJS branch. Temporarily hide `self` so it exports {Worker: FakeWorker}.
185
- // `lib` in tsconfig.json is `["ESNext"]` (no `dom`), so `self`/`document`
186
- // aren't ambiently declared on `globalThis` — narrow to just the two
187
- // properties this probe touches instead of a blanket `Record`.
188
- const g = globalThis as { self?: unknown; document?: unknown }
189
- const hadSelf = 'self' in g
190
- const origSelf = g.self
191
- if (hadSelf && typeof g.document === 'undefined') {
192
- delete g.self
193
- }
194
-
195
- elk = new ELKBundled()
196
- if (!elk) {
197
- // Unreachable — `new ELKBundled()` always returns an instance — but
198
- // makes the invariant explicit rather than letting the cast below
199
- // silently paper over a null `elk` if that ever stopped being true.
200
- throw new Error('ELKBundled construction unexpectedly produced no instance')
201
- }
202
-
203
- // Restore self
204
- if (hadSelf) g.self = origSelf
205
-
206
- // Restore setTimeout immediately
207
- globalThis.setTimeout = origSetTimeout
208
-
209
- // Flush captured callbacks synchronously — registers layout algorithms
210
- pending.forEach((fn) => fn())
211
-
212
- // Cache the raw FakeWorker for elkLayoutSync()
213
- rawWorker = (elk as ElkBundledInternal).worker.worker
214
- return rawWorker
215
- }
216
-
217
- /**
218
- * Run ELK layout synchronously.
219
- *
220
- * Bypasses BOTH of ELK's setTimeout(0) wrappers:
221
- * - FakeWorker.postMessage wraps dispatch in setTimeout(0) — bypassed by
222
- * calling dispatcher.saveDispatch() directly
223
- * - PromisedWorker.onmessage wraps receive in setTimeout(0) — bypassed by
224
- * replacing rawWorker.onmessage with a direct interceptor
225
- *
226
- * @param cache - Optional opt-in layout cache (see `createLayoutCache()`).
227
- * When provided, a cache hit returns the previous result without running
228
- * ELK layout again. Omitted/undefined preserves the original
229
- * always-recompute behavior exactly.
230
- */
231
- export function elkLayoutSync(graph: ElkNode, cache?: LayoutCache): ElkNode {
232
- // `cache`, when provided, is always an object this module itself built
233
- // via `createLayoutCache()` above — never anything constructed outside
234
- // this package. Restated as the concrete shape here (see
235
- // `LayoutCacheShape`'s doc comment) since the public `LayoutCache` type
236
- // this parameter is declared with intentionally hides `map`/`maxSize`.
237
- const internalCache = cache as LayoutCacheShape | undefined
238
- const cacheKey = internalCache ? stableStringify(graph) : undefined
239
- if (internalCache && cacheKey !== undefined) {
240
- const cached = internalCache.map.get(cacheKey)
241
- if (cached) {
242
- // Mark as most-recently-used: Map iteration order follows insertion
243
- // order, so a delete+re-set moves this entry to the end — which is
244
- // what the LRU eviction below relies on to find the *least*
245
- // recently used entry (the current first key).
246
- internalCache.map.delete(cacheKey)
247
- internalCache.map.set(cacheKey, cached)
248
- return cached
249
- }
250
- }
251
-
252
- const worker = ensureElk()
253
-
254
- let result: ElkNode | undefined
255
- let error: unknown
256
-
257
- // Replace onmessage to intercept the result synchronously
258
- // (the dispatcher calls this directly, without setTimeout)
259
- const origOnmessage = worker.onmessage
260
- worker.onmessage = (answer: { data: ElkWorkerResponse }) => {
261
- if (answer.data.error) {
262
- error = answer.data.error
263
- } else {
264
- result = answer.data.data
265
- }
266
- }
267
-
268
- // Call dispatcher.saveDispatch directly — bypasses FakeWorker.postMessage's
269
- // setTimeout(0) wrapper. The dispatcher processes the layout synchronously
270
- // and calls rawWorker.onmessage with the result.
271
- worker.dispatcher.saveDispatch({
272
- data: { id: 0, cmd: 'layout', graph },
273
- })
274
-
275
- // Restore original handler
276
- worker.onmessage = origOnmessage
277
-
278
- if (error) throw error
279
- if (!result) throw new Error('ELK layout did not return synchronously')
280
-
281
- if (internalCache && cacheKey !== undefined) {
282
- internalCache.map.set(cacheKey, result)
283
- if (internalCache.map.size > internalCache.maxSize) {
284
- // Map iteration order is insertion order, so the first key is the
285
- // least recently used (see the recency bump on hit, above).
286
- const oldestKey = internalCache.map.keys().next().value
287
- if (oldestKey !== undefined) internalCache.map.delete(oldestKey)
288
- }
289
- }
290
-
291
- return result
292
- }