Skip to content

Commit f3379b9

Browse files
lifeartclaude
andcommitted
refactor(gxt-backend): graduate __gxtInTriggerReRender to typed withInTriggerReRender + isInTriggerReRender bridge pair (Cluster B slice 18)
Promotes the `__gxtInTriggerReRender` save-restore writers (`compile.ts`'s in-line toggle inside the canonical `triggerReRender` body, slice-15 fold of core.ts's pre-slice-15 wrap; `metal/property_events.ts:96-101` caller-side toggle around `gxtTrigger(obj, keyName)`) to a typed `compilePipeline.withInTriggerReRender<T>(fn): T` helper on the gxt-bridge. Adds the paired `compilePipeline.isInTriggerReRender(): boolean` read-side predicate and routes the `metal/computed.ts:522` CP.get re-entrance guard through it. The two writers used the same `wasInside`-save / set-true / restore pattern; this helper folds that pattern into one documented bridge surface. Writer + reader audit (pre-slice-18): Writers (set the flag, save+restore via try/finally): - compile.ts:3130-3136 — wraps `_gxtTriggerReRenderBody` inside the canonical `_gxtTriggerReRender` body. This is the slice-15 fold of core.ts's pre-slice-15 `ensureTriggerReRenderWrapped` wrap. - metal/property_events.ts:96-101 — wraps the `gxtTrigger(obj, keyName)` call inside `notifyPropertyChange`. Mirrors the canonical-body wrap so callers that invoke the trigger via globalThis (rather than through the bridge) still observe `true` for the duration of the synchronous notify cascade — including any nested `notifyPropertyChange` calls produced by `__gxtTriggerReRender`'s cellFor cascades. Readers (read the flag as `=== true`): - metal/computed.ts:522 — `CP.get` short-circuits cache misses when `__gxtInTriggerReRender === true && revision === undefined`. Preserves classic Ember's "don't eagerly evaluate never-consumed CPs during a change notification" semantic. MIGRATED to `compilePipeline.isInTriggerReRender()` with globalThis fallback. - @ember/object/core.ts:325 — DEBUG proxy trap's `_isInternalPath` predicate. NOT migrated in this slice — `@ember/object/core.ts` has no pre-existing `gxt-bridge` import edge, and the surrounding predicate already reads other globalThis flags (`__gxtSyncing`, `__gxtIsRendering`) raw. Migrating one flag while leaving the others would not improve the edge count net. Slice 18 keeps this reader on globalThis, matching slice-15/17's "RETAINED for cross-package readers" precedent. Bridge shape decision: save-restore wrapper (`withInTriggerReRender<T>(fn): T`) + read-only predicate (`isInTriggerReRender(): boolean`). The writers both do the save-set-true-restore dance — a wrapper captures that exactly. The readers want a fast boolean check — a predicate is the minimal surface. The "three-method begin/end/is" alternative was rejected: the writers ALWAYS save+restore in `try/finally`, so a method pair without enforced pairing would invite drift. The wrapper enforces the pairing structurally. Namespace decision: `compilePipeline`. The flag is semantically a scope- modifier on the `triggerReRender` trigger — the compile.ts writer lives inside the canonical `triggerReRender` body, the property_events.ts writer wraps the call to `triggerReRender`, the computed.ts reader gates a CP.get short-circuit specifically for the "are we inside a trigger" question. Same namespace as slice 17's `withTriggerSuppressed` (the structural twin — both are scope-modifiers on the trigger; one suppresses the function, the other toggles the predicate). Bridge interface evolution (slice 18 — thirteenth API change): `GxtCompilePipelineCapabilities` extended with two new optional generic methods: - `withInTriggerReRender?<T>(fn: () => T): T` - `isInTriggerReRender?(): boolean` No new install API needed (reuses slice-6's `installCompilePipelinePart`). The `withTriggerSuppressed` doc comment is preserved as-is. Sites moved: - packages/@ember/-internals/gxt-backend/compile.ts: add `_gxtWithInTriggerReRender<T>` + `_gxtIsInTriggerReRender` definitions; replace the in-line save-restore inside `_gxtTriggerReRender` with a `_gxtWithInTriggerReRender(() => _gxtTriggerReRenderBody(...))` call; contribute both helpers via `installCompilePipelinePart`. - packages/@ember/-internals/gxt-backend/gxt-bridge.ts: add `withInTriggerReRender?<T>(fn): T` + `isInTriggerReRender?(): boolean` to `GxtCompilePipelineCapabilities` with slice-18 doc comments covering the writer + reader audit and the unmigrated `@ember/object/core.ts:325` reader. - packages/@ember/-internals/metal/lib/property_events.ts: import `getGxtRenderer` from `@ember/-internals/gxt-backend/gxt-bridge` (new intra-metal-lib edge — joins property_set.ts and tracked.ts's existing edges); route the `gxtTrigger(obj, keyName)` wrap through `compilePipeline.withInTriggerReRender(fn)` with inline save-restore fallback for the bridge-not-yet-installed window. - packages/@ember/-internals/metal/lib/computed.ts: import `getGxtRenderer` from `@ember/-internals/gxt-backend/gxt-bridge` (new intra-metal-lib edge); read the `__gxtInTriggerReRender` flag via `compilePipeline.isInTriggerReRender()` with raw-globalThis fallback for the bridge-not-yet-installed window. The `__gxtInTriggerReRender` globalThis writer is RETAINED post-slice-18 because of the unmigrated `@ember/object/core.ts:325` reader. Both bridge writers continue to mirror to the globalThis slot so the unmigrated reader observes the same value as the bridge-route readers. Verification (all 6 baseline gates green): - smoke: 333/333 - Errors thrown during render: 4/4 - Tracked Properties: 33/36 (matches baseline — 3 pre-existing Helper failures) - computed: 147/148 (matches baseline — 1 pre-existing) - Lifecycle: 40/42 (matches baseline — 2 pre-existing Component Context failures) - render: 977/981 (matches baseline — 4 pre-existing) Count delta: +2 bridge methods (`withInTriggerReRender` + `isInTriggerReRender`); 0 globalThis writers removed (writer retained for unmigrated core.ts reader); +2 new intra-metal-lib import edges to `gxt-bridge` (property_events.ts, computed.ts). Cumulative across Cluster B: 18 slices migrated, bridge API evolved 13 times. All 8 capabilities namespaces stable; the two new edges join the existing metal-lib edges (property_set.ts, tracked.ts) — pattern is established. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8126ba5 commit f3379b9

4 files changed

Lines changed: 170 additions & 13 deletions

File tree

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

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,38 @@ function _gxtAddAfterTriggerReRender(fn: _TriggerReRenderHook): () => void {
31093109
};
31103110
}
31113111

3112+
// Slice-18 (Cluster B): typed save-restore helper that graduates the two
3113+
// pre-slice-18 writers of `__gxtInTriggerReRender` to a bridge method:
3114+
// - the in-line toggle wrapped around `_gxtTriggerReRenderBody` below
3115+
// (the slice-15 fold of core.ts's pre-slice-15 wrap), AND
3116+
// - `metal/property_events.ts:96-101` (caller-side around `gxtTrigger`).
3117+
// Both writers used the same `wasInside`-save / set-true / restore pattern;
3118+
// this helper encapsulates it. Re-entrancy-safe because the saved value is
3119+
// whatever the enclosing frame wrote (nested calls stack correctly). The
3120+
// globalThis slot is also written so the unmigrated reader at
3121+
// `@ember/object/core.ts:325` (DEBUG proxy trap) keeps observing the same
3122+
// value as the bridge-route readers. See `withInTriggerReRender` doc in
3123+
// gxt-bridge.ts.
3124+
function _gxtWithInTriggerReRender<T>(fn: () => T): T {
3125+
const g: any = globalThis as any;
3126+
const wasInside = g.__gxtInTriggerReRender;
3127+
g.__gxtInTriggerReRender = true;
3128+
try {
3129+
return fn();
3130+
} finally {
3131+
g.__gxtInTriggerReRender = wasInside;
3132+
}
3133+
}
3134+
3135+
// Slice-18 (Cluster B): read-side predicate paired with
3136+
// `withInTriggerReRender`. Returns `true` iff the current sync stack is
3137+
// nested inside a `withInTriggerReRender(fn)` frame (which always wraps the
3138+
// canonical `triggerReRender` body). Used by `metal/computed.ts:522`'s
3139+
// CP.get re-entrance guard. Hot path: one boolean compare, zero allocations.
3140+
function _gxtIsInTriggerReRender(): boolean {
3141+
return (globalThis as any).__gxtInTriggerReRender === true;
3142+
}
3143+
31123144
const _gxtTriggerReRender = function (obj: object, keyName: string) {
31133145
// Slice-15: dispatch the BEFORE-chain (empty-chain check is a
31143146
// length-zero short-circuit so per-call overhead stays at one cmp + one
@@ -3127,13 +3159,18 @@ const _gxtTriggerReRender = function (obj: object, keyName: string) {
31273159
// `@ember/object/core.ts:70` that wrapped the global trigger; subsuming the
31283160
// toggle here lets us delete that wrap entirely. The save/restore is
31293161
// re-entrancy-safe (`wasInside` preserves an enclosing toggle if any).
3130-
const _g_around: any = globalThis as any;
3131-
const _wasInside = _g_around.__gxtInTriggerReRender;
3132-
_g_around.__gxtInTriggerReRender = true;
3162+
//
3163+
// Slice-18 (Cluster B): the in-line save-restore is replaced with the
3164+
// typed `_gxtWithInTriggerReRender` helper (which is also published on
3165+
// the bridge as `compilePipeline.withInTriggerReRender`). The flag is
3166+
// restored before the AFTER-hook chain dispatches (matching the
3167+
// pre-slice-18 ordering — AFTER hooks run with the flag already
3168+
// restored).
31333169
try {
3134-
_gxtTriggerReRenderBody(obj, keyName);
3170+
_gxtWithInTriggerReRender(() => {
3171+
_gxtTriggerReRenderBody(obj, keyName);
3172+
});
31353173
} finally {
3136-
_g_around.__gxtInTriggerReRender = _wasInside;
31373174
if (_afterTriggerReRender.length > 0) {
31383175
for (let i = 0; i < _afterTriggerReRender.length; i++) {
31393176
try {
@@ -14331,6 +14368,17 @@ installCompilePipelinePart({
1433114368
// { g.__gxtTriggerReRender = saved; }` dance at `validator.ts:117` and
1433214369
// `manager.ts:11219`. See `withTriggerSuppressed` doc in gxt-bridge.ts.
1433314370
withTriggerSuppressed: _gxtWithTriggerSuppressed,
14371+
// Slice-18 (Cluster B): typed save-restore helper for the two
14372+
// `__gxtInTriggerReRender` writers (this file's canonical-body fold of
14373+
// core.ts's pre-slice-15 wrap, and `metal/property_events.ts:96-101`'s
14374+
// caller-side toggle). The paired read-only predicate
14375+
// `isInTriggerReRender()` serves `metal/computed.ts:522`'s CP.get
14376+
// re-entrance guard. The globalThis writer is RETAINED post-slice-18
14377+
// because of the unmigrated `@ember/object/core.ts:325` DEBUG proxy-trap
14378+
// reader. See `withInTriggerReRender` / `isInTriggerReRender` doc in
14379+
// gxt-bridge.ts.
14380+
withInTriggerReRender: _gxtWithInTriggerReRender,
14381+
isInTriggerReRender: _gxtIsInTriggerReRender,
1433414382
});
1433514383

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

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,71 @@ export interface GxtCompilePipelineCapabilities {
786786
* Removing the globalThis writer is a separate (larger) migration.
787787
*/
788788
withTriggerSuppressed?<T>(fn: () => T): T;
789+
790+
/**
791+
* Run `fn` with `__gxtInTriggerReRender` set to `true` (save the prior
792+
* value, set the flag, invoke `fn`, then restore the prior value via
793+
* `try/finally`). Returns whatever `fn` returns. Re-entrancy-safe because
794+
* an enclosing frame's value is preserved by the save-restore pattern.
795+
*
796+
* Slice-18 (Cluster B): graduates the two save-restore writer sites for
797+
* `__gxtInTriggerReRender` to a typed bridge helper:
798+
* - `compile.ts:3130-3136` (folded inside the canonical `triggerReRender`
799+
* body — see slice-15 doc above): wraps the entire trigger body so any
800+
* nested `CP.get` short-circuit reads `true`.
801+
* - `metal/property_events.ts:96-101` (caller-side around the
802+
* `gxtTrigger(obj, keyName)` call in `notifyPropertyChange`): wraps the
803+
* trigger invocation so the call sites observe `true` for the duration
804+
* of the synchronous notify cascade (including any nested
805+
* `notifyPropertyChange` calls produced by `__gxtTriggerReRender`).
806+
* Both writers used to manually inline
807+
* `wasInside = g.__gxtInTriggerReRender; g.__gxtInTriggerReRender = true;
808+
* try { ... } finally { g.__gxtInTriggerReRender = wasInside; }`. This
809+
* helper folds that pattern into one documented bridge surface.
810+
*
811+
* Reader contract: the flag is consumed by:
812+
* - `metal/computed.ts:522` — `CP.get` short-circuits cache misses when
813+
* `__gxtInTriggerReRender === true && revision === undefined` (preserves
814+
* classic Ember's "don't eagerly evaluate never-consumed CPs during a
815+
* change notification" semantic). Migrated to `isInTriggerReRender()`
816+
* in slice 18 (with globalThis fallback for the bridge-not-yet-installed
817+
* edge).
818+
* - `@ember/object/core.ts:325` — DEBUG proxy trap's `_isInternalPath`
819+
* predicate. NOT migrated in slice 18 — `@ember/object/core.ts` has no
820+
* pre-existing `gxt-bridge` import edge, and the surrounding predicate
821+
* already reads other globalThis flags (`__gxtSyncing`,
822+
* `__gxtIsRendering`) raw. Migrating one flag while leaving the others
823+
* would not improve the edge count net. Slice 18 keeps this reader on
824+
* globalThis, matching slice-15/17's "RETAINED for cross-package
825+
* readers" precedent. A future cluster-D pass over the proxy trap could
826+
* migrate the whole predicate at once.
827+
*
828+
* The globalThis writer for `__gxtInTriggerReRender` is RETAINED post-
829+
* slice-18 because of the unmigrated `@ember/object/core.ts:325` reader.
830+
* Both bridge writers continue to mirror to the globalThis slot so the
831+
* unmigrated reader observes the same value as the bridge-route readers.
832+
* See `isInTriggerReRender()` below for the read-side surface.
833+
*/
834+
withInTriggerReRender?<T>(fn: () => T): T;
835+
836+
/**
837+
* Read-only predicate for `__gxtInTriggerReRender`. Returns `true` iff the
838+
* current synchronous stack is nested inside a `withInTriggerReRender(fn)`
839+
* frame (or inside the canonical `triggerReRender` body, which is itself
840+
* wrapped by `withInTriggerReRender`).
841+
*
842+
* Slice-18 (Cluster B): exposes the read side of the flag as a bridge
843+
* predicate so `metal/computed.ts`'s `CP.get` re-entrance guard can avoid
844+
* touching `globalThis` directly. The implementation reads the same
845+
* `globalThis.__gxtInTriggerReRender` slot that `withInTriggerReRender`
846+
* writes, so semantics are preserved across the unmigrated
847+
* `@ember/object/core.ts:325` reader.
848+
*
849+
* Fast-check: the implementation is `return g.__gxtInTriggerReRender ===
850+
* true` — one boolean comparison; zero allocations. Suitable for the
851+
* `CP.get` hot path.
852+
*/
853+
isInTriggerReRender?(): boolean;
789854
}
790855

791856
/**

packages/@ember/-internals/metal/lib/computed.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ import {
4040
notifyPropertyChange,
4141
PROPERTY_DID_CHANGE,
4242
} from './property_events';
43+
// Slice-18 (Cluster B): CP.get's `__gxtInTriggerReRender` re-entrance guard
44+
// reads through the typed `compilePipeline.isInTriggerReRender()` bridge
45+
// predicate. The globalThis fallback is preserved for the (rare) case where
46+
// the bridge hasn't been populated yet (early CP reads during module init).
47+
// See `isInTriggerReRender` doc in gxt-bridge.ts.
48+
import { getGxtRenderer } from '@ember/-internals/gxt-backend/gxt-bridge';
4349

4450
export type ComputedPropertyGetterFunction = (this: any, key: string) => unknown;
4551
export type ComputedPropertySetterFunction = (
@@ -519,7 +525,18 @@ export class ComputedProperty extends ComputedDescriptor {
519525
// dependent keys, which causes async observers that were registered
520526
// on CPs the caller never read to fire spuriously. Return the stored
521527
// (or undefined) value and let the next genuine lazy read recompute.
522-
if (g.__gxtInTriggerReRender === true && revision === undefined) {
528+
//
529+
// Slice-18 (Cluster B): read the flag through the typed
530+
// `compilePipeline.isInTriggerReRender()` bridge predicate. Falls
531+
// back to the raw `g.__gxtInTriggerReRender === true` slot read for
532+
// the (rare) case where the bridge hasn't been populated yet (the
533+
// bridge writer at compile.ts module-init mirrors the same slot, so
534+
// the two are equivalent post-install). See `isInTriggerReRender`
535+
// doc in gxt-bridge.ts.
536+
const _isIn = getGxtRenderer()?.compilePipeline.isInTriggerReRender;
537+
const _inTrigger =
538+
typeof _isIn === 'function' ? _isIn() : g.__gxtInTriggerReRender === true;
539+
if (_inTrigger && revision === undefined) {
523540
let stored = meta.valueFor(keyName);
524541
consumeTag(propertyTag);
525542
return stored;

packages/@ember/-internals/metal/lib/property_events.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import type { Meta } from '@ember/-internals/meta';
22
import { peekMeta } from '@ember/-internals/meta';
33
import { assert } from '@ember/debug';
4+
// Slice-18 (Cluster B): `notifyPropertyChange` routes its
5+
// `__gxtInTriggerReRender` save-restore writer through the typed
6+
// `compilePipeline.withInTriggerReRender(fn)` bridge helper. The inline
7+
// save-restore fallback is preserved for the (rare) case where the bridge
8+
// hasn't been populated yet — should only happen before compile.ts's
9+
// module-init `installCompilePipelinePart` call has fired, and
10+
// `notifyPropertyChange` is not reachable during that window in any known
11+
// entry point. See `withInTriggerReRender` doc in gxt-bridge.ts.
12+
import { getGxtRenderer } from '@ember/-internals/gxt-backend/gxt-bridge';
413
import {
514
flushSyncObservers,
615
resumeObserverDeactivation,
@@ -92,13 +101,31 @@ function notifyPropertyChange(
92101
// evaluate never-consumed CPs during a change notification" semantics.
93102
// (core.ts's lazy wrapper is only installed for proxied CoreObjects,
94103
// which misses plain `class Foo { @computed ... }` cases.)
95-
const gRoot: any = globalThis as any;
96-
const wasInside = gRoot.__gxtInTriggerReRender;
97-
gRoot.__gxtInTriggerReRender = true;
98-
try {
99-
gxtTrigger(obj, keyName);
100-
} finally {
101-
gRoot.__gxtInTriggerReRender = wasInside;
104+
//
105+
// Slice-18 (Cluster B): the `__gxtInTriggerReRender` save-restore
106+
// toggle that wraps the `gxtTrigger(obj, keyName)` call is routed
107+
// through the typed `compilePipeline.withInTriggerReRender(fn)`
108+
// bridge helper. The inline save-restore fallback is preserved for
109+
// the (rare) case where the bridge hasn't been populated yet (the
110+
// canonical body's wrap in compile.ts also sets the flag, so a
111+
// dropped caller-side wrap would only matter for the
112+
// `gxtTrigger === undefined`-after-suppression path which short-
113+
// circuits this branch anyway). See `withInTriggerReRender` doc in
114+
// gxt-bridge.ts.
115+
const _withIn = getGxtRenderer()?.compilePipeline.withInTriggerReRender;
116+
if (_withIn) {
117+
_withIn(() => {
118+
gxtTrigger(obj, keyName);
119+
});
120+
} else {
121+
const gRoot: any = globalThis as any;
122+
const wasInside = gRoot.__gxtInTriggerReRender;
123+
gRoot.__gxtInTriggerReRender = true;
124+
try {
125+
gxtTrigger(obj, keyName);
126+
} finally {
127+
gRoot.__gxtInTriggerReRender = wasInside;
128+
}
102129
}
103130
}
104131
}

0 commit comments

Comments
 (0)