Skip to content

Commit 9743bd7

Browse files
lifeartclaude
andcommitted
refactor(gxt-backend): migrate __dcChangeListeners Set + counter via compilePipeline bridge (Cluster B slice 14)
Completes the half-migrated leftover from slice 12: the dynamic-component change-listener Set (`__dcChangeListeners`) and its string-path counter (`__dcStringListenerCount`) move from globalThis-shared state to manager.ts module-local state, exposed as three new methods on `compilePipeline`. Closes the last cross-file globalThis Set in the slice-12 family. Bridge interface evolution (slice 14 — ninth API change since the pilot): - `GxtCompilePipelineCapabilities` extended with three required members: * `addDynamicComponentListener(fn, options?: { stringPath?: boolean })` -> `() => void` (off-fn). The `stringPath` option bumps the counter consulted by `hasStringDynamicComponentListeners()`. * `clearDynamicComponentListeners()` — clears both Set + counter in lockstep. * `hasStringDynamicComponentListeners(): boolean` — derived getter over the manager.ts module-local counter. - No new install API needed (manager.ts seeds all three via the initial `setGxtRenderer` call; no host-hook is contributed from outside manager.ts). Site audit (per slice 12 + 13 deferred notes — all confirmed): 1. Three writer sites in `ember-gxt-wrappers.ts`: - L1874-1877 (null path): `_nullListener` added via `add(_nullListener)`. - L2034-2037 (curried path): `_dcChangeListener` added. - L2302-2307 (string path): `_dcChangeListener` added + counter bumped. 2. Three cleanup sites in same file (L1881 / L2042 / L2312-2313): inline `delete(...)` plus the string-path counter decrement at L2313. 3. One Set reader: the dispatch in manager.ts's `_gxtSyncAllWrappers` after-body (L3712-3721, folded by slice 12). 4. Two counter readers: - manager.ts:3857 — arg-cell update path triggers `notifyPropertyChange` when string-path listeners are present. - compile.ts:5202 — Phase 1 morph-skip when string-path listeners are present. 5. One cross-test clear: compile.ts:5684-5685 plus the counter reset at compile.ts:5692. No external readers (the Set + counter were intra-gxt-backend only — confirmed by exhaustive grep across packages and tests/HTML), so dual exposure is NOT retained. The globalThis `__dcChangeListeners` / `__dcStringListenerCount` keys are removed outright (no orphan reader risk). Hooks migrated (1 Set + 1 counter -> 3 bridge methods): 1. `__dcChangeListeners` Set + `__dcStringListenerCount` counter -> `compilePipeline.addDynamicComponentListener` / `clearDynamicComponentListeners` / `hasStringDynamicComponentListeners`. - manager.ts module-local: `const _dcChangeListeners = new Set<_DcListener>()` and `let _dcStringListenerCount = 0`. The three named functions `_gxtAddDynamicComponentListener` / `_gxtClearDynamicComponentListeners` / `_gxtHasStringDynamicComponentListeners` are seeded into the bridge via `setGxtRenderer` at file EOF. - The Set dispatch in `_gxtSyncAllWrappers` after-body now iterates the module-local Set directly (no globalThis read). - The counter check in manager.ts:3857 (notifyPropertyChange dispatch after arg-cell updates) now calls `_gxtHasStringDynamicComponentListeners()`. - compile.ts:5202's morph-skip check migrates to `getGxtRenderer()?.compilePipeline.hasStringDynamicComponentListeners?.()`. - compile.ts:5684-5692's cross-test clear block (Set `.clear()` + counter reset) collapses into a single `getGxtRenderer()?.compilePipeline.clearDynamicComponentListeners?.()` call. - The three ember-gxt-wrappers.ts writer sites are replaced with a single `addDynamicComponentListener(fn, ...)` call returning an off-fn used by the existing cleanup paths. The string-path site passes `{ stringPath: true }` so the counter increments/decrements stay in lockstep with the Set add/delete inside a single bridge surface. Approach decision: (a) module-local + bridge methods, NOT (b) host-hook or (c) relocation. The Set + counter are pure state; there is no wrap-by- reassignment to break apart. The natural shape is "registry + register-fn + clear-fn + size-fn" — mechanically identical to slice 6's compile-side state-bridging, but flipped (the state lives in manager.ts, not compile.ts, because the dispatch already lives in manager.ts's `_gxtSyncAllWrappers`). Verification (all 6 baseline gates green post-slice-14): - smoke: 333/333 (16.8s) - Errors thrown during render: 4/4 - Tracked Properties: 33/36 (3 pre-existing) - computed: 147/148 (1 pre-existing) - Lifecycle: 40/42 (2 pre-existing) - render: 977/981 (4 pre-existing) Net: 0 regressions, 0 new fixes. Cluster B progress: 14 slices migrated covering 30 hooks (29 -> 30: the Set + counter count as a single logical hook expressed as 3 bridge methods) across ~76 call sites + 9 orphan cleanups + 9 wrap-by-reassignment installers eliminated (cumulative). Bridge interface evolved NINE times total (slices 6/7/8/9/10/11/12/13/14). All 8 capabilities namespaces remain stable. Slice 14 validates a SIXTH migration shape on the bridge pattern: pure state migration with a registry-style API (register-fn returning off-fn + clear-fn + size-fn). Previous shapes: slice 3 = relocation, slice 6/7/9 = install-API contribution, slice 8/10/11 = host-hook, slice 12/13 = wrap- relocation. Slice 14's shape is the first to expose mutable state behind typed methods without any host-hook or relocation pattern. Suggested next slice (slice 15): `__gxtTriggerReRender` — multi-contributor wrap (DEFERRED since slice 10 candidate ranking). The function is defined in compile.ts; manager.ts wraps it at runtime to record dirtied nested objects (`_dirtiedNestedObjectsForHooks` Set, an intra-manager.ts closure). Audit needed: count of additional wrappers (beyond manager.ts's), contributor location for each, and closure inventory. If manager.ts is the only wrap contributor, this is a clean host-hook migration (slice 8/10/11 pattern: `__gxtTriggerReRender` becomes the canonical function in `compilePipeline`, manager.ts contributes a `beforeTriggerReRender` host hook via `installCompilePipelinePart`). If multiple files wrap it, consider promoting to a chain pattern (extension of slice 8's host-hook shape) or do a multi-slice migration. Alternative: `__gxtOriginalManagers` (slice 7-deferred dual-writer) — `gxt-with-runtime-hbs.ts:219` AND `compile.ts:6023` both write it. A dedicated slice can handle the dual- write semantics via a chain or last-writer-wins discriminator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 39cf0b0 commit 9743bd7

4 files changed

Lines changed: 210 additions & 50 deletions

File tree

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5199,7 +5199,12 @@ function _resetTemplateOnlyState() {
51995199
// via cell tracking. Skip the force-rerender morph (Phase 2b) only when
52005200
// cell-based listeners exist. CurriedComponent listeners use manual DOM
52015201
// swap and need the morph for other property changes to propagate.
5202-
if ((globalThis as any).__dcStringListenerCount > 0) {
5202+
// Slice-14 (Cluster B): consult the bridge-exposed
5203+
// `hasStringDynamicComponentListeners()` rather than the pre-slice-14
5204+
// globalThis counter.
5205+
if (
5206+
getGxtRenderer()?.compilePipeline.hasStringDynamicComponentListeners?.()
5207+
) {
52035208
(globalThis as any).__gxtHadPendingSync = false;
52045209
}
52055210
}
@@ -5679,17 +5684,17 @@ setInterval(() => {
56795684
}
56805685
// Clear pending if-watcher notifications from the previous test
56815686
_pendingIfWatcherNotifications.length = 0;
5682-
// Clear dynamic component change listeners and stale getter from $_dc_ember
5687+
// Clear dynamic component change listeners and stale getter from $_dc_ember.
5688+
// Slice-14 (Cluster B): the Set + string-path counter migrated to manager.ts
5689+
// module-local state behind the bridge's
5690+
// `clearDynamicComponentListeners()` method. The bridge clear resets both
5691+
// the Set and the counter in lockstep — without that lockstep, orphaned
5692+
// listener count leaks across tests and makes __gxtSyncDomNow incorrectly
5693+
// clear __gxtHadPendingSync in Phase 1, which then causes
5694+
// __gxtForceEmberRerender to skip the morph for tests that need it (e.g.,
5695+
// classic Component.extend properties changed via set()).
56835696
(globalThis as any).__dcComponentGetter = null;
5684-
if ((globalThis as any).__dcChangeListeners) {
5685-
(globalThis as any).__dcChangeListeners.clear();
5686-
}
5687-
// Reset the string-path listener counter in lockstep with clearing the Set.
5688-
// Without this, orphaned listener count leaks across tests and makes
5689-
// __gxtSyncDomNow incorrectly clear __gxtHadPendingSync in Phase 1, which
5690-
// then causes __gxtForceEmberRerender to skip the morph for tests that
5691-
// need it (e.g., classic Component.extend properties changed via set()).
5692-
(globalThis as any).__dcStringListenerCount = 0;
5697+
getGxtRenderer()?.compilePipeline.clearDynamicComponentListeners?.();
56935698
// Clear component contexts to prevent stale render contexts accumulating
56945699
if ((globalThis as any).__gxtComponentContexts) {
56955700
(globalThis as any).__gxtComponentContexts = new WeakMap();

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

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,17 +1868,21 @@ function createEmberDc(original: Function) {
18681868
// Slice-12 (Cluster B): the pre-slice-12 wrap-by-reassignment that
18691869
// installed an inline `__gxtSyncAllWrappers` wrapper to dispatch DC
18701870
// change listeners has been REMOVED. The canonical `_gxtSyncAllWrappers`
1871-
// in manager.ts now dispatches `g.__dcChangeListeners` itself in its
1872-
// after-body (slice-3 relocation pattern). Only the Set creation + the
1873-
// `add` registration remain here.
1874-
if (!g.__dcChangeListeners) {
1875-
g.__dcChangeListeners = new Set();
1876-
}
1877-
g.__dcChangeListeners.add(_nullListener);
1871+
// in manager.ts now dispatches the change-listener Set itself in its
1872+
// after-body (slice-3 relocation pattern).
1873+
// Slice-14 (Cluster B): the Set itself moved to manager.ts module-local
1874+
// state behind the bridge's `addDynamicComponentListener` method, which
1875+
// returns an off-fn for symmetric cleanup. The pre-slice-14 inline
1876+
// `g.__dcChangeListeners.add(...)` writer + `.delete(...)` cleanup are
1877+
// replaced by `off()`.
1878+
const _offNullListener =
1879+
getGxtRenderer()?.compilePipeline.addDynamicComponentListener?.(
1880+
_nullListener
1881+
);
18781882

18791883
const _nullCleanup = () => {
18801884
_nullDestroyed = true;
1881-
g.__dcChangeListeners?.delete(_nullListener);
1885+
_offNullListener?.();
18821886
};
18831887
if (ctx && typeof gxtModule.registerDestructor === 'function') {
18841888
try {
@@ -2027,19 +2031,23 @@ function createEmberDc(original: Function) {
20272031
return true;
20282032
};
20292033

2030-
// Add listener to __dcChangeListeners (shared with null/string paths).
2031-
// Slice-12 (Cluster B): the pre-slice-12 wrap-by-reassignment installer
2032-
// has been REMOVED — `_gxtSyncAllWrappers` in manager.ts dispatches the
2033-
// Set itself in its after-body (slice-3 relocation pattern).
2034-
if (!g.__dcChangeListeners) {
2035-
g.__dcChangeListeners = new Set();
2036-
}
2037-
g.__dcChangeListeners.add(_dcChangeListener);
2034+
// Add listener to the DC-change-listener registry (shared with
2035+
// null/string paths). Slice-12 (Cluster B): the pre-slice-12
2036+
// wrap-by-reassignment installer has been REMOVED —
2037+
// `_gxtSyncAllWrappers` in manager.ts dispatches the listener Set
2038+
// itself in its after-body (slice-3 relocation pattern). Slice-14
2039+
// (Cluster B): the Set moved from globalThis to manager.ts
2040+
// module-local state behind the bridge's `addDynamicComponentListener`
2041+
// method, which returns an off-fn for symmetric cleanup.
2042+
const _offDcListener =
2043+
getGxtRenderer()?.compilePipeline.addDynamicComponentListener?.(
2044+
_dcChangeListener
2045+
);
20382046

20392047
// Cleanup destructor
20402048
const _cleanupDcListener = () => {
20412049
_dcDestroyed = true;
2042-
g.__dcChangeListeners?.delete(_dcChangeListener);
2050+
_offDcListener?.();
20432051
};
20442052
if (ctx && typeof gxtModule.registerDestructor === 'function') {
20452053
try {
@@ -2295,22 +2303,28 @@ function createEmberDc(original: Function) {
22952303
return true;
22962304
};
22972305

2298-
// Add listener to __dcChangeListeners (shared with null/curried paths).
2299-
// Slice-12 (Cluster B): the pre-slice-12 wrap-by-reassignment installer
2300-
// has been REMOVED — `_gxtSyncAllWrappers` in manager.ts dispatches the
2301-
// Set itself in its after-body (slice-3 relocation pattern).
2302-
if (!g.__dcChangeListeners) {
2303-
g.__dcChangeListeners = new Set();
2304-
}
2305-
g.__dcChangeListeners.add(_dcChangeListener);
2306-
// Track string-path listener count for morph skip logic in compile.ts
2307-
g.__dcStringListenerCount = (g.__dcStringListenerCount || 0) + 1;
2306+
// Add listener to the DC-change-listener registry (shared with
2307+
// null/curried paths). Slice-12 (Cluster B): the pre-slice-12
2308+
// wrap-by-reassignment installer has been REMOVED —
2309+
// `_gxtSyncAllWrappers` in manager.ts dispatches the listener Set
2310+
// itself in its after-body (slice-3 relocation pattern). Slice-14
2311+
// (Cluster B): the Set + string-path counter moved from globalThis to
2312+
// manager.ts module-local state behind the bridge's
2313+
// `addDynamicComponentListener` method. The `stringPath: true` option
2314+
// bumps the counter consulted by morph-skip logic in
2315+
// `__gxtSyncDomNow` / arg-cell notifyPropertyChange dispatch in
2316+
// `_gxtSyncAllWrappersBody`; the returned off-fn handles both the Set
2317+
// delete and counter decrement in lockstep.
2318+
const _offDcListener =
2319+
getGxtRenderer()?.compilePipeline.addDynamicComponentListener?.(
2320+
_dcChangeListener,
2321+
{ stringPath: true }
2322+
);
23082323

23092324
// Cleanup destructor
23102325
const _cleanupDcListener = () => {
23112326
_dcDestroyed = true;
2312-
g.__dcChangeListeners?.delete(_dcChangeListener);
2313-
if (g.__dcStringListenerCount > 0) g.__dcStringListenerCount--;
2327+
_offDcListener?.();
23142328
// NOTE: we do NOT call destroyCurrentDcInstance here — the surrounding
23152329
// render tree is being torn down by Ember, which will fire destroy
23162330
// hooks through its normal path. Calling it here would double-destroy.

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,19 @@ export interface GxtFormatCapabilities {
401401
* state — even cleaner than slice 12's globalThis-shared state — so the
402402
* relocation collapsed into a single intra-file function with zero cross-
403403
* file state references.
404+
* - `__dcChangeListeners` Set + `__dcStringListenerCount` counter — MIGRATED
405+
* IN SLICE 14 to `addDynamicComponentListener` /
406+
* `clearDynamicComponentListeners` / `hasStringDynamicComponentListeners`
407+
* on this namespace. The half-migrated leftover from slice 12: the Set's
408+
* reader was folded into manager.ts's `_gxtSyncAllWrappers` by slice 12 but
409+
* its writer sites stayed inline in ember-gxt-wrappers.ts (L1868 / L2039 /
410+
* L2317) plus a cross-test clear at compile.ts:5800-5801 and counter readers
411+
* at compile.ts:5317 + manager.ts:3713. Slice 14 promotes the Set + counter
412+
* to manager.ts module-local state and exposes typed bridge methods. NO
413+
* external readers (the Set + counter are intra-gxt-backend only —
414+
* confirmed by exhaustive grep), so dual exposure is NOT retained — the
415+
* `__dcChangeListeners` / `__dcStringListenerCount` globals are removed
416+
* outright.
404417
*/
405418
export interface GxtCompilePipelineCapabilities {
406419
/**
@@ -498,6 +511,79 @@ export interface GxtCompilePipelineCapabilities {
498511
*/
499512
clearInstancePools(): void;
500513

514+
/**
515+
* Register a dynamic-component change listener that fires AFTER every
516+
* sync-all pass (in `_gxtSyncAllWrappers`'s after-body). Listeners are
517+
* notified when dynamic-component swaps need to perform manual DOM updates
518+
* — used by the three `$_dc_ember` paths in ember-gxt-wrappers.ts:
519+
* - null path (L1862): `_nullListener` performs the null-DOM swap.
520+
* - curried path (L2024): `_dcChangeListener` performs curried swap.
521+
* - string path (L2292): `_dcChangeListener` performs string-name swap.
522+
*
523+
* Returns an "off" function the caller invokes from its destructor to
524+
* de-register the listener. The off-fn is idempotent.
525+
*
526+
* Pass `{ stringPath: true }` for the string-component path so the bridge
527+
* tracks a counter consulted by `hasStringDynamicComponentListeners()`
528+
* (compile.ts's `__gxtSyncDomNow` morph-skip logic; manager.ts's
529+
* notifyPropertyChange dispatch when string-path listeners are present).
530+
*
531+
* Slice-14 design: the listener Set + string-path counter live as
532+
* manager.ts module-local state (`_dcChangeListeners` Set,
533+
* `_dcStringListenerCount` number). The pre-slice-14 globalThis Set + counter
534+
* (`__dcChangeListeners`, `__dcStringListenerCount`) are removed outright —
535+
* no external readers exist (intra-gxt-backend only). Replaces three inline
536+
* `g.__dcChangeListeners.add(...)` writer sites at
537+
* ember-gxt-wrappers.ts:1877 / :2037 / :2305 plus the counter increment at
538+
* :2307, the cleanup `.delete(...)` sites at :1881 / :2042 / :2312, and the
539+
* decrement at :2313 — all replaced by a single `addDynamicComponentListener`
540+
* call returning the appropriate off-fn.
541+
*
542+
* Previously: `(globalThis as any).__dcChangeListeners` Set + the
543+
* `(globalThis as any).__dcStringListenerCount` counter.
544+
*/
545+
addDynamicComponentListener(
546+
fn: () => boolean,
547+
options?: { stringPath?: boolean }
548+
): () => void;
549+
550+
/**
551+
* Clear all registered dynamic-component change listeners and reset the
552+
* string-path listener counter to zero. Called from compile.ts's
553+
* `__gxtSyncDomNow` test-teardown Phase 2 (the cross-test reset block at
554+
* compile.ts:5800-5801) so listeners registered by a previous test do not
555+
* fire (and are not counted) during the next test.
556+
*
557+
* Slice-14 design: replaces the pre-slice-14 inline
558+
* `(globalThis as any).__dcChangeListeners.clear()` plus
559+
* `(globalThis as any).__dcStringListenerCount = 0` at compile.ts:5800-5801
560+
* with a single bridge call.
561+
*
562+
* Previously: `(globalThis as any).__dcChangeListeners.clear()` +
563+
* `(globalThis as any).__dcStringListenerCount = 0`.
564+
*/
565+
clearDynamicComponentListeners(): void;
566+
567+
/**
568+
* Return `true` if any STRING-path dynamic-component listener is currently
569+
* registered. Used by compile.ts's `__gxtSyncDomNow` Phase 1 to decide
570+
* whether to skip the force-rerender morph (Phase 2b) — cell-based string-
571+
* path swaps already handled dynamic-component identity via the bridge
572+
* dispatch, so the morph would re-apply already-applied changes. Also used
573+
* by manager.ts's arg-cell update path to dispatch
574+
* `notifyPropertyChange` so Ember's tag-driven @computed properties on
575+
* classic components recompute after arg updates via syncAll (the Ember tag
576+
* isn't dirtied by direct property assignment).
577+
*
578+
* Slice-14 design: derived getter over the manager.ts module-local
579+
* `_dcStringListenerCount` number. Replaces the pre-slice-14 inline
580+
* `(globalThis as any).__dcStringListenerCount > 0` checks at
581+
* compile.ts:5317 and manager.ts:3713 (the two reader sites).
582+
*
583+
* Previously: `(globalThis as any).__dcStringListenerCount > 0` inline.
584+
*/
585+
hasStringDynamicComponentListeners(): boolean;
586+
501587
/**
502588
* Compile a template string to a gxt-compatible template factory.
503589
* Contributed by compile.ts (the function definition's home file).

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

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3617,6 +3617,48 @@ function _installTriggerReRenderWrapper() {
36173617
// dispatch) into the canonical body below. All state crossed via globalThis
36183618
// (`__gxtAllPoolArrays`, `__gxtSyncCycleId`, `__gxtSyncAllInFlightCycle`,
36193619
// `__dcChangeListeners`) so no closures needed to be moved.
3620+
3621+
// Slice-14 (Cluster B): the `__dcChangeListeners` Set and
3622+
// `__dcStringListenerCount` counter that were left as globalThis-shared
3623+
// semaphores by slice 12 are now manager.ts module-local. Writers
3624+
// (`ember-gxt-wrappers.ts:1877 / :2037 / :2305`), counter readers
3625+
// (`compile.ts:5202` morph-skip, `manager.ts:3857` notifyPropertyChange
3626+
// dispatch), and the cross-test clear (`compile.ts:5684-5692`) all go through
3627+
// the bridge methods `addDynamicComponentListener` /
3628+
// `hasStringDynamicComponentListeners` / `clearDynamicComponentListeners`.
3629+
// The Set's reader (the after-body dispatch in `_gxtSyncAllWrappers`)
3630+
// iterates the module-local Set directly. No external readers exist (the Set
3631+
// + counter were intra-gxt-backend only — confirmed by exhaustive grep), so
3632+
// dual exposure is NOT retained.
3633+
type _DcListener = () => boolean;
3634+
const _dcChangeListeners = new Set<_DcListener>();
3635+
let _dcStringListenerCount = 0;
3636+
function _gxtAddDynamicComponentListener(
3637+
fn: _DcListener,
3638+
options?: { stringPath?: boolean }
3639+
): () => void {
3640+
_dcChangeListeners.add(fn);
3641+
const isStringPath = options?.stringPath === true;
3642+
if (isStringPath) {
3643+
_dcStringListenerCount++;
3644+
}
3645+
let removed = false;
3646+
return function off(): void {
3647+
if (removed) return;
3648+
removed = true;
3649+
_dcChangeListeners.delete(fn);
3650+
if (isStringPath && _dcStringListenerCount > 0) {
3651+
_dcStringListenerCount--;
3652+
}
3653+
};
3654+
}
3655+
function _gxtClearDynamicComponentListeners(): void {
3656+
_dcChangeListeners.clear();
3657+
_dcStringListenerCount = 0;
3658+
}
3659+
function _gxtHasStringDynamicComponentListeners(): boolean {
3660+
return _dcStringListenerCount > 0;
3661+
}
36203662
//
36213663
// AROUND-shape relocation (slice-3 relocation pattern, FIRST application to a
36223664
// wrap-by-reassignment exclusion — prior wraps slices 8/10/11 used the
@@ -3706,17 +3748,15 @@ function _gxtSyncAllWrappers(): void {
37063748
} finally {
37073749
// === AFTER: clear in-flight state and dispatch DC change listeners
37083750
// (relocated from compile.ts wrap finally + ember-gxt-wrappers.ts
3709-
// L1872 / L2043 / L2321 wrap bodies) ===
3751+
// L1872 / L2043 / L2321 wrap bodies; slice-14 moves the Set from
3752+
// globalThis to manager.ts module-local `_dcChangeListeners`) ===
37103753
g.__gxtSyncAllInFlightPass = 0;
37113754
g.__gxtSyncAllInFlightCycle = 0;
3712-
const dcListeners = g.__dcChangeListeners;
3713-
if (dcListeners) {
3714-
for (const listener of dcListeners) {
3715-
try {
3716-
listener();
3717-
} catch {
3718-
/* ignore */
3719-
}
3755+
for (const listener of _dcChangeListeners) {
3756+
try {
3757+
listener();
3758+
} catch {
3759+
/* ignore */
37203760
}
37213761
}
37223762
}
@@ -3854,7 +3894,7 @@ function _gxtSyncAllWrappersBody(): void {
38543894
// updates via syncAll (the Ember tag isn't dirtied by direct
38553895
// property assignment).
38563896
if (
3857-
(globalThis as any).__dcStringListenerCount > 0 &&
3897+
_gxtHasStringDynamicComponentListeners() &&
38583898
entry.instance &&
38593899
typeof entry.instance.trigger === 'function'
38603900
) {
@@ -12709,6 +12749,21 @@ setGxtRenderer({
1270912749
// state only, so the slice-3 relocation pattern applies — second
1271012750
// wrap-by-reassignment slice to use relocation after slice 12.
1271112751
clearInstancePools: _gxtClearInstancePools,
12752+
// Slice-14 (Cluster B): seeded here with the dynamic-component listener
12753+
// bridge methods. The Set (`_dcChangeListeners`) and counter
12754+
// (`_dcStringListenerCount`) live as manager.ts module-local state; the
12755+
// pre-slice-14 globalThis `__dcChangeListeners` / `__dcStringListenerCount`
12756+
// were intra-gxt-backend only (no external readers — confirmed by
12757+
// exhaustive grep), so dual exposure is NOT retained. Writers move from
12758+
// inline `g.__dcChangeListeners.add(...)` (ember-gxt-wrappers.ts L1877 /
12759+
// L2037 / L2305) to a single `addDynamicComponentListener` call that
12760+
// returns an off-fn for symmetric cleanup. The `stringPath: true`
12761+
// variant also bumps a counter consulted by
12762+
// `hasStringDynamicComponentListeners()` for compile.ts's morph-skip
12763+
// logic and manager.ts's notifyPropertyChange dispatch.
12764+
addDynamicComponentListener: _gxtAddDynamicComponentListener,
12765+
clearDynamicComponentListeners: _gxtClearDynamicComponentListeners,
12766+
hasStringDynamicComponentListeners: _gxtHasStringDynamicComponentListeners,
1271212767
},
1271312768
renderPass: {
1271412769
// Slice-8: triad seeded here; the `beforeBeginRenderPass` host hook is

0 commit comments

Comments
 (0)