Skip to content

Research/upgrade ch5 for solid#276

Open
nbrar-crestron wants to merge 9 commits into
Crestron:masterfrom
nbrar-crestron:research/upgrade-ch5-for-solid
Open

Research/upgrade ch5 for solid#276
nbrar-crestron wants to merge 9 commits into
Crestron:masterfrom
nbrar-crestron:research/upgrade-ch5-for-solid

Conversation

@nbrar-crestron
Copy link
Copy Markdown

An AI based review.

nbrar-crestron and others added 9 commits May 20, 2026 10:43
Adds Jest 29 + ts-jest + jest-environment-jsdom as a new test runner that
lives next to the existing mocha (.spec.ts) and WCT (HTML) suites. New
behavioural / before-after tests for the SOLID upgrade go in tests/jest/.

  npm run test:jest             # one-shot
  npm run test:jest:watch       # watch mode
  npm run test:jest:coverage    # with coverage

Jest's testMatch picks up only tests/jest/**/*.test.ts and explicitly
ignores src/**/*.spec.ts, so the mocha suite is unaffected.

tsconfig.umd.json gains an exclude for tests/ — its prior **/*.ts include
was greedy enough to pull the new Jest tests into the library build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on guard

src/ch5-list/ch5-list.ts:508 had setTimeout(..., 0.5) — almost certainly
a typo for 500. A 0.5 ms delay collapses to the next macrotask, defeating
the debounce and forcing a synchronous resizeList() on every
viewport-change event.

Adds tests/jest/perf/short-timeout.test.ts, a regression guard that walks
every src/*.ts via the TypeScript compiler API and flags any setTimeout /
setInterval call whose delay is a numeric literal in (0, 16) ms. The
threshold is one frame at 60Hz. delay === 0 is permitted as the standard
"yield to next macrotask" idiom.

Supporting helpers:
  tests/jest/_helpers/source-scanner.ts  — file walker + line/col resolver
  tests/jest/_helpers/ts-call-finder.ts  — AST-based call-site finder

The AST approach handles nested calls, multi-line arguments, and comments
correctly — a regex pass over the same code missed multi-line setTimeout
and over-matched on inner literals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… singleton

Adds src/ch5-core/ch5-shared-mutation-observer.ts — a singleton pool that
wraps a single browser-level MutationObserver and dispatches per-target
callbacks. Modelled on the existing Ch5CoreIntersectionObserver pattern.

Refactors src/ch5-common/ch5-mutation-observer.ts to be a thin facade
that delegates to the pool. The constructor, observe(), disconnectObserver(),
isConnected field, ELEMENTS_MO_EXCEPTION static, and checkElementValidity
static are preserved exactly — so the 35+ existing call sites need no
changes.

Net effect: N components that each used to construct their own
MutationObserver now share ONE underlying observer. Proven by
tests/jest/perf/mutation-observer-facade.test.ts (50 facade instances →
1 ctor call).

Dispatch correctly handles subtree:true configurations by walking up
mutation.target's ancestor chain to find a matching registered node.

Test coverage:
  shared-mutation-observer.test.ts    7 tests — pool invariants
  mutation-observer-facade.test.ts    5 tests — back-compat + isolation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ility

Adds src/ch5-core/ch5-shared-resize-observer.ts — a singleton pool that
wraps a single browser ResizeObserver and routes entries to per-target
callbacks. Includes feature-detection so it's a no-op on platforms
without native ResizeObserver.

Refactors src/ch5-core/resize-observer.ts. The historical utility
created a new ResizeObserver per call AND returned void — every caller
leaked one observer permanently with no way to disconnect. It now:
  • delegates to Ch5SharedResizeObserver (one underlying observer)
  • returns a disposer so opt-in callers can finally clean up

The 11 existing callers that ignore the return value still work and
still benefit from pooling. Two direct consumers are migrated to use
the disposer in their removeEventListeners() path:
  src/ch5-video-switcher/ch5-video-switcher.ts
  src/ch5-signal-level-gauge/ch5-signal-level-gauge.ts

Test coverage (9 tests):
  shared-resize-observer.test.ts
    • N targets → 1 underlying ResizeObserver
    • per-target dispatch
    • disposer unregisters exactly that target
    • teardown on empty
    • graceful no-op when ResizeObserver is unavailable
    • legacy utility routes through the pool
    • legacy utility returns a working disposer
    • legacy utility still works for callers that ignore the disposer

Supporting helper:
  tests/jest/_helpers/fake-resize-observer.ts — controllable shim for JSDOM
  (JSDOM does not implement ResizeObserver natively)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…init failures

Two long-standing bugs that the test scaffolding now guards against.

1. Language-change subscription leak (src/ch5-common/ch5-common.ts)

The Ch5Common constructor subscribed to the language-change signal and
dropped the returned subscription key on the floor. The subscription
closure captured `this` (the component) AND `this.translatableObjects`,
so every component instance was pinned in memory for the lifetime of
the page — even after removal from the DOM.

Fix: track the returned key in `_languageChangeSubKey` and release it
in `unsubscribeFromSignals()` via `signal.unsubscribe(key)`. Always
released — the `_keepListeningOnSignalsAfterRemoval` flag is for
receive-state bridge subs used for re-attachment; the language sub
must not be kept alive.

Also adds `_baseClassSubscriptions: Subscription[]` as future
infrastructure for any non-bridge RxJS subscriptions a base or
subclass method might register. Drained alongside the language sub.

2. Silent catches in ColorPicker (src/ch5-color-picker/color-picker.ts)

The constructor's try/catch swallowed init failures into `joe = null`
with no log. setColor() then silently no-op'd on the null. QA and
field engineers had nothing to grep for when the picker didn't render.

Fix:
  • catches now console.warn with the picker id and the underlying error
  • renamed private `joe` → `_picker` (descriptive; nothing external
    referenced the old name)
  • added `isReady` getter so callers can check before calling
  • setColor() returns boolean (existing void-returning callers unaffected)

Test coverage:
  language-subscription.test.ts   4 tests — bridge unsubscribe, drain
                                            array, _keepListening flag,
                                            no-op when nothing tracked
  color-picker-silent-catch.test.ts 2 tests — init failure warns + isReady,
                                              setColor warns on uninit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eslint:
  • no-explicit-any: off → warn
  • no-console:       off → warn

Surfaces ~689 warnings across the existing source so future work can
chip away at them without blocking the build today.

package.json:
  • removes the `lint` npm script — it referenced `tslint`, but no
    tslint.json exists in the repo. eslint (script `eslint`) is the
    real linter.

MIGRATION.md:
  • developer-facing walkthrough of every change in this branch
  • how to run the new Jest harness
  • the new shared observer APIs and their migration path
  • the bridge-vs-RxJS subscription pattern in Ch5Common
  • the regression-guard tests and how to add new ones
  • what is explicitly deferred to later phases and why

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion analysis to MIGRATION.md, capturing the Crestron#1 critical perf
finding from the architecture review: every list-family component
(ch5-list, ch5-button-list, ch5-subpage-reference-list, ch5-spinner)
renders every item to the DOM regardless of viewport.

The document covers:
  • why it hurts specifically on panel hardware (vs. desktop)
  • what windowing-with-recycling looks like for CH5
  • the four hard parts (variable heights, focus/gesture, signal
    rebinding, behavioural parity)
  • proposed Phase 2 / Phase 3 sequencing
  • a pre-flight checklist of questions to resolve with the team
    before any code gets written
  • how this work builds on Phase 1's shared observers, leak fix,
    and Jest harness

Intended as a starting point for team review of what Phase 2 should
look like.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third in the architecture-review doc series, after MIGRATION.md
(Phase 1 inventory) and LIST_VIRTUALIZATION.md (Phase 2 case for
list family).

Compares <ch5-button> per-instance runtime cost against Bootstrap's
.btn, targeted at the Crestron TSW-770 panel. Hard numbers, cited
to file:line in src/ch5-button/ and src/ch5-common/, with the
asymmetry between the two components honestly acknowledged: Bootstrap
is mostly CSS; ch5-button is a reactive component bound to the
signal bus.

Findings:
  • DOM nodes per button:   1 (Bootstrap)  vs.  6–9 (ch5)
  • Heap objects on init:   0–1            vs.  20–30
  • Native listeners:        0–1 delegated  vs.  8–10 per instance
  • Browser observers:       0              vs.  2 per instance on master
  • Signal subscriptions:    0              vs.  10+ per instance
  • Memory:                  < 1 KB         vs.  15–25 KB per instance

Identifies the three worst hot spots (full classList enumeration on
every render; per-instance MutationObserver+ResizeObserver;
innerHTML='' rebuilds on label change) and proposes seven
improvements ranked by ROI, plus a sequencing plan that lands the
biggest wins inside ch5-button-base.ts before the Phase 2
maintainability refactor.

Includes an "Open questions for the team" section: visual parity,
mode-state churn rate in real projects, signal-subscription coverage,
and pressable semantics — all worth resolving before any PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourth in the architecture-review doc series, after MIGRATION.md,
LIST_VIRTUALIZATION.md, and BUTTON_PERFORMANCE.md.

Picks up the open question from BUTTON_PERFORMANCE.md: can ch5-button
allocate only what its authored markup actually asks for, rather than
spinning up the full 20–30 object / 10+ signal-subscription surface on
every instance?

Distinguishes two complementary techniques:

  A. Runtime-lazy composition — a feature registry where each
     capability (label, receiveStateLabel, sendEventOnClick, mode,
     pressable, …) is a small installable unit triggered by the
     presence of its attribute.

  B. Build-time pruning — the HTML5 UI Builder already sees every
     <ch5-button> in every project; it can emit a manifest of used
     capabilities and tree-shake the bundle accordingly.

Includes:
  • a concrete TypeScript sketch of the feature-registry pattern,
  • a side-by-side pros table covering memory, observers, signal
    subs, bundle size, render time, authoring compatibility,
  • seven cons / risks with mitigations,
  • CH5-specific considerations (toolchain as a unique asset; Lit /
    FAST validation of the pattern),
  • a falsifiable two-week proof-of-concept plan that ships against
    one capability before committing to the full refactor,
  • open questions for the team and links back to the rest of the
    doc series.

Intended as a design-discussion starting point. No code shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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