Skip to content

Commit a96e264

Browse files
lifeartclaude
andcommitted
refactor(gxt-backend): graduate __gxtIsRendering predicate to compilePipeline.isRendering bridge (Cluster B slice 19)
Promotes the render-pass-depth read-side predicate (`__gxtIsRendering`) to a typed `compilePipeline.isRendering(): boolean` method on the gxt-bridge. The single writer is `__gxtSetIsRendering` (also defined in `compile.ts`, mutated cross-package from `glimmer/lib/renderer.ts:2236/2272/2283`); it remains on globalThis in this slice — a future slice 20 can promote the writer to a paired begin/end (or `withRendering(fn)`) helper. Writer + reader audit (pre-slice-19): Writer (1): - `compile.ts:_gxtSetIsRendering` — increment on `true`, decrement on `false`. Mutates the module-local `_renderPassDepth` counter that backs both `__gxtIsRendering` and `_gxtIsRendering`. Called from `glimmer/lib/renderer.ts:2236/2272/2283` via globalThis (cross-package writer; renderer wraps each `template.render()` with `setIsRendering(true)` + restore in `try/finally`). Readers (4): - `compile.ts:1903` ($_inElement render-pass detect for deferred in-element renders). MIGRATED to intra-file `_gxtIsRendering()`. - `compile.ts:2130` ($_inElement self-insert heuristic for in- element rendering into an empty target). MIGRATED to intra-file `_gxtIsRendering()`. - `glimmer/lib/renderer.ts:2233/2271` (renderComponent's `wasRendering` save-restore + inner `_doRender` reactor's `wasRenderingLocal` check — line 2271 captures the same `_isRendering` closure variable as line 2233, so a single edit at line 2233 propagates). MIGRATED to `compilePipeline.isRendering()` bridge call with globalThis fallback. - `@ember/object/core.ts:321` (DEBUG proxy trap `_isInternalPath` predicate). NOT migrated in this slice — the trap reads three globalThis flags together (`__gxtIsRendering`, `__gxtSyncing`, `__gxtInTriggerReRender`); migrating only one would not improve the edge count net. Slice 18 deferred the same trap for its own `__gxtInTriggerReRender` reader. Suggested slice 20 migrates the whole 3-flag predicate at once. Bridge shape decision: single read-only predicate `isRendering(): boolean` mirroring slice-18's `isInTriggerReRender()`. The writer (`_gxtSetIsRendering`) is paired begin/end via cross-package callers in `glimmer/lib/renderer.ts`, not a `withRendering(fn)` save-restore helper. Promoting the writer to a paired begin/end bridge surface is a separate concern (and touches the cross-package renderer.ts writer site) — deferred to slice 20. Namespace decision: `compilePipeline`. The flag is semantically a scope-modifier on the GXT template render pipeline — the writer + depth counter live in `compile.ts` (the pipeline's home file), the intra-package readers are in `$_inElement` rendering helpers, and the cross-package reader in `glimmer/lib/renderer.ts` is the renderer's wrap of `template.render()`. Same namespace as slices 15/17/18. Bridge interface evolution (slice 19 — fourteenth API change): `GxtCompilePipelineCapabilities` extended with one new optional method: - `isRendering?(): boolean` No new install API needed (reuses slice-6's `installCompilePipelinePart`). Sites moved: - packages/@ember/-internals/gxt-backend/compile.ts: promote `_renderPassDepth` and the two functions (`_gxtIsRendering` / `_gxtSetIsRendering`) to module-local scope (previously closure-locals inside the `if (typeof __gxtIsRendering !== 'function')` guard); preserve the dual-load guard around the globalThis writers; migrate the two intra-file readers ($_inElement render-pass detect at 1903 and self-insert heuristic at 2130) to direct `_gxtIsRendering()` calls; contribute `isRendering: _gxtIsRendering` via `installCompilePipelinePart`. - packages/@ember/-internals/gxt-backend/gxt-bridge.ts: add `isRendering?(): boolean` to `GxtCompilePipelineCapabilities` with slice-19 doc comments covering the writer + reader audit and the unmigrated `@ember/object/core.ts:321` reader. - packages/@ember/-internals/glimmer/lib/renderer.ts: route the `_isRendering` capture in renderComponent (line 2233) through `getGxtRenderer()?.compilePipeline.isRendering` with globalThis fallback. The inner `_doRender` reactor's `wasRenderingLocal` check (line 2271 / now 2285) captures the same `_isRendering` closure variable, so the single edit propagates. The `__gxtIsRendering` globalThis writer is RETAINED post-slice-19 because of the unmigrated `@ember/object/core.ts:321` reader. Both the bridge predicate and the globalThis function reference the same module-local `_renderPassDepth` counter — they are equivalent post- install. Hot-path note: `_gxtIsRendering` is `return _renderPassDepth > 0` — one integer comparison; zero allocations. The bridge route in renderer.ts adds one property lookup per `renderComponent` call — not a tight loop. Intra-compile.ts readers in $_inElement use the direct function call (no bridge indirection) which is strictly faster than the pre-slice-19 globalThis lookup. Verification (all 6 baseline gates green): - smoke: 333/333 - Errors thrown during render: 4/4 - Tracked Properties: 33/36 (baseline) - computed: 147/148 (baseline) - Lifecycle: 40/42 (baseline) - render: 977/981 (baseline) Count delta: +1 bridge method (`isRendering`); 0 globalThis writers removed (writer retained for unmigrated core.ts reader); 0 new import edges (renderer.ts already imports `getGxtRenderer`; intra- compile.ts readers use module-local function). Cumulative across Cluster B: 19 slices migrated, bridge API evolved 14 times. Suggested slice 20: migrate the `@ember/object/core.ts:321-326` DEBUG proxy trap `_isInternalPath` predicate as a unit. The trap reads three globalThis flags together (`__gxtIsRendering`, `__gxtSyncing`, `__gxtInTriggerReRender`) — migrating them individually has been deferred across slices 18 and 19 because the edge-count math doesn't improve. A focused slice 20 can either (a) add a single composite predicate `isInGxtInternalPath(propName): boolean` to the bridge, or (b) add the missing `isSyncing()` predicate and have core.ts call all three bridge predicates (which also enables migrating the `__gxtSyncing` writer separately). After slice 20 closes the trap, slice 21 can promote `__gxtSetIsRendering` to a paired `beginRendering()/endRendering()` (or `withRendering(fn)`) bridge writer surface, dropping the globalThis writer entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f3379b9 commit a96e264

3 files changed

Lines changed: 146 additions & 32 deletions

File tree

packages/@ember/-internals/glimmer/lib/renderer.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2229,8 +2229,22 @@ function _renderComponentGxt(
22292229
// Save and restore the previous isRendering state to avoid clobbering
22302230
// it when renderComponent is called from within another render pass
22312231
// (e.g., from a component constructor during an outer template render).
2232+
//
2233+
// Slice-19 (Cluster B): read the render-pass depth through the typed
2234+
// `compilePipeline.isRendering()` bridge predicate. Falls back to the
2235+
// raw `globalThis.__gxtIsRendering` lookup for the (rare) case where
2236+
// the bridge hasn't been populated yet (the bridge writer at compile.ts
2237+
// module-init contributes the same `_gxtIsRendering` function reference
2238+
// as the globalThis writer, so the two are equivalent post-install).
2239+
// The `__gxtSetIsRendering` writer is left on globalThis pending a
2240+
// future slice 20 begin/end migration. See `isRendering` doc in
2241+
// gxt-bridge.ts.
22322242
const _setRendering = (globalThis as any).__gxtSetIsRendering;
2233-
const _isRendering = (globalThis as any).__gxtIsRendering;
2243+
const _bridgeIsRendering = getGxtRenderer()?.compilePipeline.isRendering;
2244+
const _isRendering =
2245+
typeof _bridgeIsRendering === 'function'
2246+
? _bridgeIsRendering
2247+
: ((globalThis as any).__gxtIsRendering as (() => boolean) | undefined);
22342248
const wasRendering = typeof _isRendering === 'function' ? _isRendering() : false;
22352249
if (typeof _setRendering === 'function') {
22362250
_setRendering(true);

packages/@ember/-internals/gxt-backend/compile.ts

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,32 +1713,51 @@ if (false as boolean) {
17131713
// a tracked boolean. Also wire compile.ts's own render() wrapper to
17141714
// bump it, because precompileTemplate's render() is called from the
17151715
// renderer.ts code path.
1716-
if (typeof (globalThis as any).__gxtIsRendering !== 'function') {
1717-
let _renderPassDepth = 0;
1718-
(globalThis as any).__gxtSetIsRendering = function (on: boolean) {
1719-
if (on) _renderPassDepth++;
1720-
else if (_renderPassDepth > 0) {
1721-
_renderPassDepth--;
1722-
// When the top-level render pass ends (depth 1 → 0) the parent's
1723-
// DOM fragment has been committed to its target. Drain any
1724-
// in-element deferred renders that were queued during this pass —
1725-
// by now their compile-time literal id targets are resolvable via
1726-
// document.getElementById. This is the synchronous drain point for
1727-
// the renderComponent strict-mode path, which does NOT go through
1728-
// __gxtRebuildViewTreeFromDom or __gxtFlushAfterInsertQueue.
1729-
if (_renderPassDepth === 0) {
1730-
try {
1731-
const drain = (globalThis as any).__gxtInElementDrainDeferred;
1732-
if (typeof drain === 'function') drain();
1733-
} catch {
1734-
/* ignore */
1735-
}
1716+
//
1717+
// Slice-19 (Cluster B): the read-side predicate `_gxtIsRendering` is
1718+
// promoted to module-local scope and contributed to the bridge as
1719+
// `compilePipeline.isRendering()` (see `installCompilePipelinePart` at the
1720+
// bottom of this file). The globalThis writers (`__gxtIsRendering` /
1721+
// `__gxtSetIsRendering`) are RETAINED:
1722+
// - `__gxtIsRendering` is retained because the unmigrated reader at
1723+
// `@ember/object/core.ts:321` (DEBUG proxy trap) reads three flags
1724+
// together (`__gxtIsRendering`, `__gxtSyncing`, `__gxtInTriggerReRender`);
1725+
// migrating one flag while leaving the other two raw would not improve
1726+
// the edge count net. Slice 18 already deferred the same trap for its
1727+
// own `__gxtInTriggerReRender` reader. A future slice (suggested slice
1728+
// 20) can migrate the whole 3-flag predicate at once.
1729+
// - `__gxtSetIsRendering` is retained because the cross-package writer at
1730+
// `glimmer/lib/renderer.ts:2232/2272/2282` toggles it via globalThis and
1731+
// a future slice should migrate it as a paired `beginRendering()` /
1732+
// `endRendering()` helper (see suggested slice 20 in the memory notes).
1733+
let _renderPassDepth = 0;
1734+
function _gxtIsRendering(): boolean {
1735+
return _renderPassDepth > 0;
1736+
}
1737+
function _gxtSetIsRendering(on: boolean): void {
1738+
if (on) _renderPassDepth++;
1739+
else if (_renderPassDepth > 0) {
1740+
_renderPassDepth--;
1741+
// When the top-level render pass ends (depth 1 → 0) the parent's
1742+
// DOM fragment has been committed to its target. Drain any
1743+
// in-element deferred renders that were queued during this pass —
1744+
// by now their compile-time literal id targets are resolvable via
1745+
// document.getElementById. This is the synchronous drain point for
1746+
// the renderComponent strict-mode path, which does NOT go through
1747+
// __gxtRebuildViewTreeFromDom or __gxtFlushAfterInsertQueue.
1748+
if (_renderPassDepth === 0) {
1749+
try {
1750+
const drain = (globalThis as any).__gxtInElementDrainDeferred;
1751+
if (typeof drain === 'function') drain();
1752+
} catch {
1753+
/* ignore */
17361754
}
17371755
}
1738-
};
1739-
(globalThis as any).__gxtIsRendering = function () {
1740-
return _renderPassDepth > 0;
1741-
};
1756+
}
1757+
}
1758+
if (typeof (globalThis as any).__gxtIsRendering !== 'function') {
1759+
(globalThis as any).__gxtSetIsRendering = _gxtSetIsRendering;
1760+
(globalThis as any).__gxtIsRendering = _gxtIsRendering;
17421761
}
17431762

17441763
// Deferred in-element render queue. When $_inElement encounters a null
@@ -1894,15 +1913,17 @@ if (typeof (globalThis as any).__gxtIsRendering !== 'function') {
18941913
// whose outer DOM fragment hasn't been committed to the live document
18951914
// yet, `document.getElementById` returns null even though the target
18961915
// element exists in the pending parent fragment. Detect this by
1897-
// checking __gxtIsRendering() — if true, we're mid-render and should
1916+
// checking _gxtIsRendering() — if true, we're mid-render and should
18981917
// defer the block body render until after the parent commits.
18991918
//
19001919
// Outside of an active render pass (classic `this.render` assertion
19011920
// tests that pass `this.someElement = null`), fall through to the
19021921
// synchronous assertion so expectAssertion() still catches the throw.
1903-
const _gxtIsRenderingFn = (globalThis as any).__gxtIsRendering;
1904-
const _insideRenderPass =
1905-
typeof _gxtIsRenderingFn === 'function' && _gxtIsRenderingFn() === true;
1922+
//
1923+
// Slice-19 (Cluster B): intra-compile.ts reader — call the module-local
1924+
// `_gxtIsRendering` directly (which is also published on the bridge as
1925+
// `compilePipeline.isRendering()`). One call, zero globalThis lookups.
1926+
const _insideRenderPass = _gxtIsRendering();
19061927
// Only defer if we have a compile-time literal id fallback to
19071928
// re-resolve with. Without one, we cannot distinguish "render-order
19081929
// timing bug" from "user actually passed null" — and deferring
@@ -2127,7 +2148,9 @@ if (typeof (globalThis as any).__gxtIsRendering !== 'function') {
21272148
// We detect self-insertion heuristically: the target is empty AND we
21282149
// are currently inside a render pass. Standard `{{#in-element
21292150
// externalTarget}}` into a non-rendering target is unaffected.
2130-
const gxtRenderingFn = (globalThis as any).__gxtIsRendering;
2151+
// Slice-19 (Cluster B): intra-compile.ts reader — call the module-local
2152+
// `_gxtIsRendering` directly (which is also published on the bridge as
2153+
// `compilePipeline.isRendering()`).
21312154
// Suppress self-insert when a delegate explicitly tells us the current
21322155
// render target is *not* the in-element destination. The rehydration
21332156
// delegate sets __gxtInElementRenderTarget to the element it's
@@ -2144,8 +2167,7 @@ if (typeof (globalThis as any).__gxtIsRendering !== 'function') {
21442167
insertBefore === null &&
21452168
appendRef.childNodes.length === 0 &&
21462169
!isExternalTarget &&
2147-
typeof gxtRenderingFn === 'function' &&
2148-
gxtRenderingFn() === true;
2170+
_gxtIsRendering();
21492171
if (isSelfInsert) {
21502172
// Return a fragment containing [content..., placeholder]. GXT's
21512173
// outer-template commit will insert this whole fragment at the
@@ -14379,6 +14401,15 @@ installCompilePipelinePart({
1437914401
// gxt-bridge.ts.
1438014402
withInTriggerReRender: _gxtWithInTriggerReRender,
1438114403
isInTriggerReRender: _gxtIsInTriggerReRender,
14404+
// Slice-19 (Cluster B): read-only predicate for the render-pass depth
14405+
// counter (`_renderPassDepth` defined near the top of this file). Replaces
14406+
// the cross-package globalThis lookup at `glimmer/lib/renderer.ts:2233`
14407+
// (used to decide whether to suppress text-effect creation during nested
14408+
// renderComponent calls). The globalThis writer `__gxtIsRendering` is
14409+
// RETAINED post-slice-19 because of the unmigrated
14410+
// `@ember/object/core.ts:321` DEBUG proxy-trap reader (same trap that
14411+
// slice 18 deferred). See `isRendering` doc in gxt-bridge.ts.
14412+
isRendering: _gxtIsRendering,
1438214413
});
1438314414

1438414415
// Slice-8 (Cluster B): replaces the pre-slice-8 `_installTemplateOnlyResetHook`

packages/@ember/-internals/gxt-backend/gxt-bridge.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,75 @@ export interface GxtCompilePipelineCapabilities {
851851
* `CP.get` hot path.
852852
*/
853853
isInTriggerReRender?(): boolean;
854+
855+
/**
856+
* Read-only predicate for the render-pass depth counter at
857+
* `compile.ts:_renderPassDepth`. Returns `true` iff at least one render
858+
* pass is currently active (i.e., `_gxtSetIsRendering(true)` was called
859+
* more often than `_gxtSetIsRendering(false)` since module init).
860+
*
861+
* Slice-19 (Cluster B): graduates the single-writer / multi-reader render-
862+
* pass-detect flag (`__gxtIsRendering`) to a typed read-side predicate.
863+
* The writer (`compile.ts:_gxtSetIsRendering`, exposed as
864+
* `globalThis.__gxtSetIsRendering`) is the only mutator of the depth
865+
* counter; this predicate is the read-side surface.
866+
*
867+
* Writer + reader audit (pre-slice-19):
868+
* Writer (1):
869+
* - `compile.ts:_gxtSetIsRendering` — increment on `true`, decrement
870+
* on `false`. Called from:
871+
* - `glimmer/lib/renderer.ts:2236/2272/2283` via
872+
* `globalThis.__gxtSetIsRendering` (cross-package writer; the
873+
* renderer wraps each `template.render()` invocation with
874+
* `setIsRendering(true)` + restore).
875+
* - The depth counter is module-local to `compile.ts` so it is
876+
* shared between the in-element `$_inElement` readers and the
877+
* renderer's wraps (which both go through globalThis).
878+
* Readers (4):
879+
* - `compile.ts:1903` ($_inElement render-pass detect for deferred
880+
* in-element renders). MIGRATED in slice 19 to `_gxtIsRendering()`
881+
* intra-file call.
882+
* - `compile.ts:2130` ($_inElement self-insert heuristic). MIGRATED
883+
* in slice 19 to `_gxtIsRendering()` intra-file call.
884+
* - `glimmer/lib/renderer.ts:2233/2271` (renderComponent's
885+
* wasRendering save-restore + the inner `_doRender` reactor's
886+
* wasRenderingLocal check). MIGRATED in slice 19 to
887+
* `compilePipeline.isRendering()` bridge call.
888+
* - `@ember/object/core.ts:321` (DEBUG proxy trap `_isInternalPath`
889+
* predicate). NOT migrated in slice 19 — the trap reads three
890+
* globalThis flags together (`__gxtIsRendering`, `__gxtSyncing`,
891+
* `__gxtInTriggerReRender`); migrating only one would not improve
892+
* the edge count net. Slice 18 deferred the same trap for its own
893+
* `__gxtInTriggerReRender` reader. A future slice 20 can migrate
894+
* the whole 3-flag predicate at once (see suggested slice 20 in
895+
* memory notes).
896+
*
897+
* Bridge shape decision: read-only predicate (mirroring slice-18's
898+
* `isInTriggerReRender()`). The single writer (`_gxtSetIsRendering`)
899+
* is paired begin/end through cross-package callers in
900+
* `glimmer/lib/renderer.ts`, not a `withRendering(fn)` save-restore
901+
* helper. A future slice 20 can promote the writer to a paired
902+
* `beginRendering()` / `endRendering()` (or `withRendering(fn)`) bridge
903+
* surface, but slice 19 keeps the writer on globalThis to avoid touching
904+
* the cross-package renderer.ts writer site at the same time.
905+
*
906+
* Namespace decision: `compilePipeline`. The flag is semantically a
907+
* scope-modifier on the GXT template render pipeline — the writer +
908+
* depth counter live in `compile.ts` (the pipeline's home file), the
909+
* intra-package readers are in `$_inElement` rendering helpers, and the
910+
* cross-package reader in `glimmer/lib/renderer.ts` is the renderer's
911+
* wrapping of `template.render()`. Same namespace as slices 15/17/18.
912+
*
913+
* The globalThis writer `__gxtIsRendering` is RETAINED post-slice-19
914+
* because of the unmigrated `@ember/object/core.ts:321` reader. The
915+
* bridge predicate reads the same module-local `_renderPassDepth`
916+
* counter as the globalThis function — they are equivalent post-install.
917+
*
918+
* Fast-check: the implementation is `return _renderPassDepth > 0` — one
919+
* integer comparison; zero allocations. Suitable for hot-path use in
920+
* the `$_inElement` rendering helpers.
921+
*/
922+
isRendering?(): boolean;
854923
}
855924

856925
/**

0 commit comments

Comments
 (0)