Skip to content

refactor(engine): let features register their own engine handlers - #29613

Draft
chrisnojima wants to merge 1 commit into
nojima/HOTPOT-arch-10-fake-enginefrom
nojima/HOTPOT-arch-09-engine-handlers
Draft

refactor(engine): let features register their own engine handlers#29613
chrisnojima wants to merge 1 commit into
nojima/HOTPOT-arch-10-fake-enginefrom
nojima/HOTPOT-arch-09-engine-handlers

Conversation

@chrisnojima

@chrisnojima chrisnojima commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Two parallel notification-distribution mechanisms over the same action stream.

The good one is already deep: subscribeToEngineAction(type, fn) — type-indexed, HMR-safe, self-resetting, used at ~136 sites across ~27 modules.

The other was 66 hand-written case arms across three files:

File Arms
constants/init/shared.tsx 41, with 48 getState() reach-ins into 16 stores
constants/init/index.tsx 16, platform-split
stores/config.tsx 9

The switch is shallow — its interface is a discriminated union as wide as its implementation — and it inverted the dependency, making constants/init/ a compile-time dependency of every store.

Change

Each owner registers its own handlers at module init, through the same listenersByType map notifyEngineActionListeners already walks. onEngineIncoming becomes one line. constants/init/shared.tsx: 498 → 320 lines.

Verified mechanically that all 58 distinct action types are registered — none dropped, none added (case labels extracted from git show master: and diffed against every registerEngineHandlers block).

Order dependences found — now explicit priorities

sharedFirst -400 < shared -300 < config -200 < default 0 < platform 100

  1. NotifyBadges.badgeStatesyncInboxBadgeState must rebuild the inbox badge map before anything derives tab counts → sharedFirst.
  2. badgeState and gregorUI.pushState appear in both the shared and config switches, shared first.
  3. NotifySession.loggedIn/loggedOut appear in both config and the desktop platform switch; the platform arm reads userSwitching and calls getEngine().reset() after config's setLoggedIn. Sharpest one.
  4. notifyUserBlocked / trackingChanged are in the shared switch and tracker/identify-session.tsx; switch first.

Intra-arm order is preserved by keeping each arm a single handler. Audited and found not order-dependent: gregorUI.pushState's block-buttons vs notifications pair (disjoint writes).

HMR hazards fixed

  • stores/config.tsx registered inside the zustand creator, whose store instance createZustand discards on HMR — a hot reload would unregister the working handlers and install ones writing into an orphan while the app rendered from the survivor. Moved to module scope; the only one of 13 sites that was inside a creator.
  • nextSeq was module-local while listenersByType lives on globalThis under __DEV__, so a hot reload of that module alone restarted it at 0 and inverted registration order. Now on globalThis beside the map.

Deliberate drop, stated not silent

The shared switch's pushState arm recomputed the same nonNull gregor filter and emitted the same logger.warn that useNotifState already emits for the same action. One warning remains.

Validation

lint:all clean — 0 bailed out, 0 whole-props deps, tsc clean both projects.
jest --runInBand231 suites / 2251 tests (baseline 230 / 2236).

stores/tests/notifications.test.ts no longer pokes dispatch.onEngineIncomingImpl — an internal that existed only because the switch called it.

Two mechanisms distributed the same action stream. The deep one -
subscribeToEngineAction, type-indexed and HMR-safe - already had ~136
subscriptions. The shallow ones were three central switches: 41 arms in
constants/init/shared.tsx reaching into 16 stores, 13 more split by platform in
constants/init/index.tsx, and 9 inside the config store. That made
constants/init a compile-time dependency of every store it dispatched into.

Each feature now registers its own handlers at module init through the same
listenersByType the notifier already walks. The three switches are gone;
onEngineIncoming is notifyEngineActionListeners.

Order was case-arm position, which said nothing. It is now an explicit priority
on the registration: sharedFirst < shared < config < component subscriptions <
platform, matching the order those four tiers ran in before. The one arm with a
genuine internal dependency - the inbox conversation badge map has to be
rebuilt before anything derives tab counts from it - registers at sharedFirst.

Registrations are permanent: a sign-out reset drops what components subscribed,
but nothing re-runs module init to put a registration back.

The duplicate gregor nonNull filter (and its duplicate warning) that the shared
switch performed alongside the identical one in useNotifState is dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rccpV5nLxxC5opF5xzrz7
@chrisnojima
chrisnojima force-pushed the nojima/HOTPOT-arch-09-engine-handlers branch from 878c7e0 to 55bacb9 Compare September 11, 2026 01:47
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