Skip to content

refactor(group-channel): P0 runtime stability scaffolding (Phase 0-4)#1428

Draft
danney-chun wants to merge 8 commits into
mainfrom
refactor/p0-phase-4-unread-reducer
Draft

refactor(group-channel): P0 runtime stability scaffolding (Phase 0-4)#1428
danney-chun wants to merge 8 commits into
mainfrom
refactor/p0-phase-4-unread-reducer

Conversation

@danney-chun
Copy link
Copy Markdown
Contributor

Summary

Foundation for the P0 runtime-coupling refactor. Introduces an internal-only typed event reducer, scroll controller, and unread reducer alongside the existing GroupChannelProvider — all behind the internal/ boundary, with zero changes to the public API surface or scrollPubSub contract.

This PR ships the scaffolding only. Consumer migration (legacy → new reducers/controllers) is deliberately deferred to a follow-up cycle so this change can land with full BC guarantees and per-phase parity tests in place.

  • Design ref: .agentic/refactor-design.ko.md §5.8 / §21
  • Spec / Plan / Review: .agentic/p0-impl/{spec,plan,notes}.md, .agentic/p0-impl/review/code-review.md

Phase-by-phase (8 commits)

Phase Commit What
0 9b2fb628 Characterization tests, render-count baseline, BC check script
1 d050c9ba useStoreSelector (narrow-slice subscription, useSyncExternalStore + memoizing getSnapshot) + applyStorePatch (dev-instrumented patch helper)
2a fe098a52 Runtime event types + pure reducer + selectors + adapter mappers (mapChannelReady, mapOnMessagesReceived, etc.)
2b 2e0a1176 Wire runtime adapter into GroupChannelProviderparallel-only / additive; no legacy state replaced
3a 05d4cc8d ScrollIntent + ScrollAnchor + ScrollController (pure, no-op executor default)
3b 5a48f14f useMessageListScroll subscribes scrollPubSub topics → typed intents (controller side-by-side with legacy DOM handlers)
4 e78f1f2f UnreadReducer + selectors with parity tests against legacy useUnread computation
W1 96ad27dd G3 review fix: isolate dispatchToRuntime failures from legacy flow (parallel-only invariant)

BC invariants (all PASS — ./scripts/bc-check.sh)

  • BC-1 package.json public fields (name/version/main/module/types/exports/peerDependencies/dependencies) — unchanged
  • BC-2 dist/types/index.d.ts export set — unchanged
  • BC-3 Context shape baseline (useGroupChannelContext() field set) — well-formed
  • BC-4 dist/types/index.d.ts does NOT re-export anything from internal/
  • BC-5 No external source imports from internal/ (module-private boundary intact)
  • BC-6 scrollPubSub topic + payload set + call sites — unchanged

Parallel-only invariant (Plan §2.4)

A bug in any new reducer, mapper, or internal/ controller MUST NOT prevent legacy GroupChannelProvider callbacks from running. Enforced two ways:

  1. dispatchToRuntime wraps the reducer + applyStorePatch chain in try/catch; on failure it returns [] and invokes the optional onError (logger). onError is itself wrapped.
  2. GroupChannelProvider's runtimeDispatch thunk helper defers mapper execution into the guard so a mapper throw is also contained.

Verified by 4 new specs under integration.spec.ts > parallel-only invariant (W1):

  • malformed CHANNEL_READY (channel: null) → reducer throws → swallow + onError invoked
  • onError throwing does not propagate
  • runtime state is preserved when dispatch fails (reference-equal getState)
  • dispatch without onError still swallows

Test plan

  • ./scripts/bc-check.sh — BC-1 ~ BC-6 all PASS post-rebase
  • yarn jest src/modules/GroupChannel/internal/ src/utils/test/p0/ — 13 suites / 136 tests PASS
  • Full yarn jest (pre-rebase, source identical) — 194/196 PASS (2 pre-existing skipped, 0 fail), 1112/1122 + 10 skipped
  • yarn build clean (pre-existing warnings only — named/default export mix, ContextMenu circular deps)
  • 5x deterministic re-run of P0 spec set (43 suite / 310 test) before W1 — stable

Out of scope (intentional — follow-up cycles)

  • Consumer migration: legacy useUnread / scroll handlers / state slice consumers are not yet switched to read from the new reducers. The parity tests prove the new model matches legacy output; the switch will be its own PR with render-count budget checks.
  • W2 as never cast removal on mapOnMessagesReceived/mapOnMessagesUpdated — widen mapper input type to readonly BaseMessage[] so the cast is unnecessary
  • W3 useStoreSelector JSDoc: explicit guidance that consumers must pass module-level or useCallback-stabilized selectors
  • W5/W8 Phase 3 sub-batch 3 real executor wiring: requires recordAndRun rejection guard + reference-equality fast path in hasStateChanged before SCROLL_POSITION_CHANGED high-frequency dispatch begins
  • coreTs onCacheResult / onApiResult wiring: pending @sendbird/uikit-tools version bump (callbacks exist in local coreTs source but not in published 0.1.0)

Reviewer guide

  • Public API surface is provably unchanged — start with the BC-check output.
  • All new modules live under src/modules/GroupChannel/internal/ and are NOT in the package barrel.
  • The 4 modified production files are: useStore.ts (additive exports), storeManager.ts (additive exports), GroupChannelProvider.tsx (additive dispatch sites + runtimeDispatch helper), useMessageListScroll.tsx (additive controller setup).
  • The lone deletion (-1 line) is a single import line in useMessageListScroll.tsx that was expanded to add useEffect.

🤖 Generated with Claude Code

Adds the Phase 0 deliverables for the P0 runtime-coupling refactor cycle.
No production source code is modified.

Layout
- src/utils/test/p0/renderCounter.ts          — render-count instrumentation
- src/utils/test/p0/characterization/         — store + list-store harnesses
- src/__tests__/p0-characterization/          — 15 characterization specs
- src/__tests__/p0-baseline/                  — baseline snapshots (JSON+txt)
- scripts/bc-check.sh + bc-check/             — SDK backward-compat checks

Characterization coverage (14 design scenarios + ordering)
- effect-ordering                  : mark-as-read / reset / publish sequence
- scroll-position-render-boundary  : whole-context fan-out baseline
- receive-at-bottom-render-and-behavior : merged scenarios 3 + 10
- scroll-to-bottom-clears-new-messages  : scenario 7 + payload shape
- reaction-update-render-boundary  : scenario 5
- scroll-to-message-cache-hit      : scenario 8 (animated, top)
- scroll-to-message-cache-miss     : scenario 9 (animated, lazy, top)
- receive-away-from-bottom-unread  : scenario 11
- starting-point-init-scroll       : scenario 12 (cache + reset paths)
- typing-rerender-boundary         : scenario 1 (store notifyCount = 0)
- typing-status-message-input      : scenario 2 (channel-side isolation)
- typing-bubble-bottom-scroll      : scenario 13
- ios-input-focus-after-send       : scenario 14 (unit-level decomposition)
- channel-list-typing-render-boundary : scenario 6 (list-store fixture)
- context-shape-parity             : C4 useGroupChannelContext field set

Backward-compatibility checks (scripts/bc-check.sh)
- BC-1 package.json public fields diff
- BC-2 dist/types/index.d.ts export-set diff (snapshot committed)
- BC-3 context-shape-baseline.json well-formed
- BC-4 dts walker — no internal/ leak into entry dts
- BC-5 source grep — no external imports of internal/
- BC-6 scrollPubSub topic type + call sites contract

Behavior preservation
- 0 existing test file modified (git diff name-only confirms additive)
- 0 production source modified (only test harnesses and helper scripts)
- 5 consecutive isolated runs: 16/16 suite + 56/56 test PASS deterministic
- Full suite: 180/182 suites pass (2 pre-existing skipped); occasional
  pre-existing parallel flake on unrelated specs (utils/Thread) — verified
  pre-existing, not introduced by this change

Refs spec: .agentic/p0-impl/spec.md  (gitignored, not in this commit)
Refs plan: .agentic/p0-impl/plan.md  (gitignored, not in this commit)
Phase 1 of the P0 runtime-coupling refactor. Adds two new APIs alongside
the existing store surface; no existing exports are modified.

New surface
- `useStoreSelector(ctx, selector, equalityFn?)` — memoized getSnapshot
  pattern over `useSyncExternalStore` so siblings reading different store
  slices remain independent. Default equality is `Object.is`; consumers
  needing value-semantics over object slices pass a shallow/deep fn.
- `applyStorePatch(store, patch, reason, opts?)` — equality-respecting
  patch helper with a dev/test instrumentation hook
  (`globalThis.__APPLY_STORE_PATCH_HOOK__`). The `opts.bypassEquality`
  escape hatch maps to `store.setState(patch, true)` for the rare cases
  where reference identity must be re-broadcast.

Existing surface preserved
- `createStore`, `hasStateChanged`, `Store<T>`, `Store.setState(partial, force?)`
  unchanged.
- `useStore` signature and behavior unchanged.
- `setState(..., true)` call sites in `useGroupChannel.ts` and
  `useGroupChannelList.ts` remain literally as-is — Plan Review C1 invariant.

Tests
- `src/__tests__/storeManager.selector.spec.ts` — 17 cases covering
  RV-1.1..1.7 (Plan §1.1-1.3 / Spec AC-3a..c).
- All 15 Phase 0 characterization specs continue to pass unmodified.
- 5 consecutive runs: 17/17 suite + 73/73 test deterministic.
- Full suite: 182/184 suites + 982/992 tests pass; 0 existing test files
  modified.

Backward compatibility
- `dist/types/index.d.ts` export set unchanged (BC-2 PASS).
- `internal/` leak check N/A this phase (BC-4 PASS — no internal/ dir).
- Source grep for external internal/ imports clean (BC-5 PASS).
- `scrollPubSub` topic contract unchanged (BC-6 PASS).
- `package.json` public fields unchanged (BC-1 PASS).

Branched from refactor/p0-impl@5a7ff901 (Phase 0 commit).
…lectors + adapter mappers

Phase 2 of the P0 runtime-coupling refactor — pure modules only. The
GroupChannelProvider integration that wires these into the existing
GroupChannel store will land in a follow-up commit so the parity check
can be reviewed as an isolated change.

Modules (all under src/modules/GroupChannel/internal/runtime/, all gated
from public dts re-export by BC-4):
- events.ts       — 15-variant GroupChannelRuntimeEvent union, plus
                    auxiliary types (ScrollPosition, ScrollMetrics,
                    BrowserResumeReason, ChannelClearReason, etc.)
                    and ALL_RUNTIME_EVENT_TYPES exhaustive constant.
- state.ts        — GroupChannelRuntimeState shape with channel,
                    collection, messages, scroll, browser slices.
                    Shared frozen empty sentinels for structural sharing.
- reducer.ts      — pure groupChannelRuntimeReducer producing
                    {state, effects[]}. Structural sharing across no-op
                    transitions. Exhaustiveness guard via never.
- selectors.ts    — narrow-slice selectors (selectChannelStatus,
                    selectMessages, selectIsScrollAtBottom, etc.) for
                    Phase 1's useStoreSelector consumers.
- adapter.ts      — coreTs callback → RuntimeEvent mappers (map-prefixed
                    functions, one per callback boundary) and
                    toGroupChannelState() outbound derivation.

Tests (under __tests__/, all new files):
- reducer.transitions.spec.ts (23 cases) — AC-6 / RV-2.3..2.6, all 15
  event variants, structural sharing, purity.
- adapter.mapping.spec.ts (19 cases) — RV-2.1 / RV-2.2 coreTs → event
  mapping table; RV-2.7 toGroupChannelState parity field set.
- selectors.spec.ts (7 cases) — slice readers + identity stability.

coreTs contract verification (Plan §2.0 prereq) is recorded in
.agentic/p0-impl/notes.md with file:line citations. Result: proceed —
all needed dispatch sources are either existing callbacks or additive
supplies (onCacheResult / onApiResult / onMessagesUpdated /
onCollectionEvent). coreTs zero change.

Backward compatibility (verified by ./scripts/bc-check.sh)
- BC-1 package.json public fields unchanged.
- BC-2 dist/types/index.d.ts export set unchanged (post-build).
- BC-4 dist/types/index.d.ts does not re-export from internal/.
- BC-5 no external source imports of internal/.
- BC-6 scrollPubSub topic contract unchanged.

Stability
- 5 consecutive runs: 122/122 tests deterministic (Phase 0 + 1 + 2).
- Full suite: 185/187 suites pass (2 pre-existing skipped, 0 fail).
- Phase 0 characterization specs unchanged and passing.
- Phase 1 store-guardrail specs unchanged and passing.
- 0 existing test files modified.

Branched from refactor/p0-phase-1-store-guardrail@019f5888.
…parallel-only)

Phase 2 integration — additive-only changes to GroupChannelProvider so the
runtime reducer/adapter introduced in 4f71740a starts receiving dispatched
events at every coreTs callback boundary. The runtime store is parallel
to the legacy GroupChannelContext store; nothing in the legacy code path
is modified, and no legacy state is yet driven by the runtime reducer.
Phase 3 (ScrollController) and Phase 4 (UnreadReducer) will consume from
this dispatch boundary.

New files
- internal/runtime/integration.ts             — createRuntimeStore +
  dispatchToRuntime, with a dev/test instrumentation hook
  (__GROUP_CHANNEL_RUNTIME_DISPATCH_HOOK__) so RV specs can observe
  events / state / effects.
- internal/runtime/__tests__/integration.spec.ts — 6 cases verifying
  store construction, dispatch effects, hook firing, error swallowing,
  and subscriber notification.

GroupChannelProvider.tsx modifications (all additive)
- New imports from `../internal/runtime/{integration,adapter}`.
- `useRef(createRuntimeStore())` inside GroupChannelManager for the
  per-mount runtime store.
- Dispatch calls added BEFORE existing legacy actions at six boundaries:
    onMessagesReceived → MESSAGES_ADDED
    onMessagesUpdated  → MESSAGES_UPDATED   (NEW additive coreTs callback)
    onChannelDeleted   → CHANNEL_CLEARED(deleted)
    onCurrentUserBanned→ CHANNEL_CLEARED(banned)
    onChannelUpdated   → CHANNEL_READY (updated channel)
    getChannel resolve → CHANNEL_READY
    getChannel reject  → CHANNEL_FAILED
- No existing line in GroupChannelManager is rewritten — only insertions.

Behavior preservation
- 0 existing test file modified (AC-0a — git diff confirms).
- 982/992 existing tests still pass (10 pre-existing skipped, 0 fail).
- Phase 0 characterization specs (15 files, 47 tests) unmodified and
  passing — proves visible behavior is unchanged.
- Phase 1 store-guardrail specs unmodified and passing.
- useGroupChannelContext() field set unchanged (Phase 0
  context-shape-parity spec confirms).
- scrollPubSub topic + payload contract unchanged (BC-6 PASS).

coreTs version note
- onCacheResult / onApiResult exist in the local coreTs checkout but not
  in published @sendbird/uikit-tools@0.1.0. COLLECTION_CACHE_RESULT and
  COLLECTION_API_RESULT runtime events remain reducer-only until a
  follow-up cycle bumps the dependency. Documented inline at the
  useGroupChannelMessages call site.

Backward compatibility
- BC-5 grep tightened: `internal/` imports from within the SAME module
  (e.g., GroupChannel/context reaching into GroupChannel/internal) are
  allowed; only cross-module reaches are flagged. Same-module wiring is
  the very purpose of the `internal/` boundary.
- BC-1 package.json fields unchanged.
- BC-2 dist/types/index.d.ts export set unchanged.
- BC-3 context-shape-baseline.json unchanged.
- BC-4 dist/types/index.d.ts does not re-export from internal/.
- BC-6 scrollPubSub topic contract unchanged.

Stability
- 5 consecutive isolated runs: 36 suite / 239 test deterministic.
- Full suite: 186/188 suites pass (2 pre-existing skipped, 0 fail).
- TypeScript clean (npx tsc --noEmit).
- ESLint clean (npx eslint internal/ + GroupChannelProvider.tsx).

Phase 2 mini-gate (Plan §"Mini-Gates"):
  1. Behavior      ✓ existing tests unmodified + passing
  2. Characterization ✓ 15 specs unmodified + passing
  3. RV (Phase 2)  ✓ 49 reducer/adapter/selector/integration cases pass
  4. BC-1..6       ✓ all PASS
  5. type / lint   ✓ clean

Branched from refactor/p0-phase-2-runtime-adapter@4f71740a (Phase 2
pure modules). Phase 3 will subscribe to the runtime store for scroll
intent consumption.
…troller (pure)

Phase 3 sub-batch 1 of the P0 runtime-coupling refactor — pure modules
only. The scrollPubSub bridge integration into useMessageListScroll will
land in a follow-up commit so the parallel-path observation can be
reviewed as an isolated change.

Modules (all under src/modules/GroupChannel/internal/scroll/, all gated
from public dts re-export by BC-4):
- intents.ts        — ScrollIntent (5 variants: TO_BOTTOM, TO_MESSAGE,
                      PRESERVE_ANCHOR, RESTORE_AFTER_RESIZE, NONE) +
                      ScrollAnchor (3 kinds: bottom, message,
                      distanceFromBottom) + AnchorStrategy, ScrollMetrics,
                      ContentSizeChangeReason, ViewportChangeReason.
                      Plus ALL_SCROLL_INTENT_TYPES exhaustive constant.
- anchors.ts        — pure selectAnchor(strategy, metrics, messages,
                      target) implementing the 5-strategy table from
                      design §6.5 (force-bottom, force-distance,
                      target-message, nearest-visible-message, auto).
- controller.ts     — createScrollController factory. attach/measure/
                      getAnchor/run/notifyContentSizeChanged/
                      notifyViewportChanged surface. Pluggable executor
                      defaults to no-op (Phase 3 sub-batch 2 will plug
                      in the real DOM executor). Intent log + global
                      hook (__GROUP_CHANNEL_SCROLL_CONTROLLER_HOOK__)
                      for test/dev observability.
- browserViewport.ts — feature-detected window.resize /
                      window.orientationchange / visualViewport.resize /
                      visualViewport.scroll observer. SSR-safe (no-op
                      when window is undefined).

Tests (all new __tests__/ files):
- anchors.spec.ts            (11 cases) — RV-3.5 strategy table.
- controller.intents.spec.ts (11 cases) — RV-3.1..3.3 partial:
  surface contract, measure, position categorization, run + hook,
  intent log, notifyContentSizeChanged → PRESERVE_ANCHOR,
  notifyViewportChanged → RESTORE_AFTER_RESIZE, hook error swallow.
- browserViewport.spec.ts    (5 cases)  — listener wiring + dispose.
- intents.spec.ts            (1 case)   — exhaustive type list.

Phase 3 invariants preserved
- scrollPubSub topic / payload contract unchanged (BC-6 PASS — type
  literal set + call site set + snapshot all match).
- Phase 0 characterization specs (15 files, 47 tests) unmodified and
  passing.
- Phase 1 store-guardrail specs unmodified and passing.
- Phase 2 runtime adapter specs unmodified and passing.
- 0 existing test file modified.
- 0 production source modified — controller is built and unit-tested
  in isolation; sub-batch 2 wires it into useMessageListScroll.tsx.

Backward compatibility
- BC-1 package.json public fields unchanged.
- BC-2 dist/types/index.d.ts export set unchanged (post-build verified).
- BC-3 context-shape-baseline.json present.
- BC-4 dist/types/index.d.ts does not re-export from internal/.
- BC-5 no external source imports from internal/ (module-private intact).
- BC-6 scrollPubSub topic contract unchanged.

Stability
- 5 consecutive runs: 40 suite / 268 test deterministic (Phase 0+1+2+3).
- Full suite: 190/192 suites pass (2 pre-existing skipped, 0 fail).
- TypeScript clean (npx tsc --noEmit).
- ESLint clean.

Branched from refactor/p0-phase-2-runtime-adapter@79019a8a.
…ler (parallel-only)

Phase 3 sub-batch 2 — wires the ScrollController introduced in 268a1f65
into useMessageListScroll. Every scrollPubSub publish now also produces a
typed ScrollIntent on the controller's intent log; the controller's
default no-op executor ensures the legacy DOM handlers below remain the
sole driver of actual scrollTop mutation. Phase 4+ may swap consumers
to the controller path once characterization confirms parity.

useMessageListScroll.tsx changes (all additive)
- New imports: createScrollController, attachViewportObserver.
- `useRef(createScrollController())` per hook mount.
- `useLayoutEffect` for `controller.attach(scrollRef.current)` (idempotent).
- `useEffect` to attach the viewport observer (SSR-safe; feature-detects
  window.visualViewport). Returns a cleanup that disposes listeners.
- A second `useLayoutEffect` adds parallel scrollPubSub subscribers that
  translate each publish into the corresponding ScrollIntent
  (`TO_BOTTOM` / `TO_MESSAGE`) and forward to `controller.run(...)`.
- Existing DOM scroll handlers and the public hook return shape are
  untouched.

Mapping (RV-3.1 / RV-3.2 verified)
- publish('scrollToBottom', { animated, resolve })
    → controller.run({ type: 'TO_BOTTOM', animated, reason: 'button', resolve })
- publish('scroll', { top, animated, lazy, resolve })
    → controller.run({ type: 'TO_MESSAGE', createdAt: 0, animated,
                       focus: false, top, lazy, resolve })

New test
- internal/scroll/__tests__/pubSubBridge.spec.tsx (5 cases)
  RV-3.1 / RV-3.2 + parallel-path proof + unmount cleanup.

Backward compatibility
- scrollPubSub topic + payload shape UNCHANGED (BC-6 PASS).
- useMessageListScroll return shape UNCHANGED (no new fields exposed).
- useGroupChannelContext field set UNCHANGED (context-shape-parity
  spec passes).
- Phase 0 characterization (15 specs, 47 tests) unmodified and passing.
- Phase 1 / Phase 2 specs unmodified and passing.
- 0 existing test file modified.
- BC-1..6 all PASS (post `yarn build`).
- TypeScript clean / ESLint clean.

Stability
- 5 consecutive runs: 41 suite / 273 test deterministic.
- Full suite: 191/193 suites pass (2 pre-existing skipped, 0 fail).

Known follow-ups (deferred to a later cycle, not blocking Phase 3)
- MessageList.onMessageContentSizeChanged and InfiniteList previous/next
  load currently mutate scrollTop directly OR publish via scrollPubSub.
  Direct-mutate paths bypass the bridge; a future RV-3.3/3.4 spec would
  cover those after MessageList/InfiniteList learn about the controller.

Branched from refactor/p0-phase-3-scroll-controller@268a1f65.
…se 4)

Phase 4 of the P0 runtime-coupling refactor — final phase. Introduces a
single typed model for the unread/separator/mark-as-read domain so the
three legacy sources (coreTs `newMessages`, `MessageList.unreadSinceDate`,
`GroupChannelState.firstUnreadMessageId`) can be derived from one
authoritative state. Pure modules only — Plan §4.2 commits zero consumer
migration in this cycle. A post-cycle phase migrates the consumers after
the parity suite has been observed to hold over time.

Modules (all under src/modules/GroupChannel/internal/unread/, gated by
BC-4 / BC-5):
- model.ts        — UnreadState (mode/anchor/ids/count/visibility/markRead
                    flags/lastReadAt), UnreadMode union, frozen empty Set
                    sentinel, createInitialUnreadState factory.
- reducer.ts      — pure unreadReducer covering 7 events:
                    USER_LEFT_BOTTOM, USER_REACHED_BOTTOM,
                    MESSAGES_RECEIVED, MESSAGES_DELETED,
                    MARK_AS_UNREAD_SET, READ_CONFIRMED, CHANNEL_CHANGED.
                    Rules from design §7.6: sender filtering, bottom
                    behavior, sticky marked-unread, delete reconciliation.
- selectors.ts    — 5 selectors per design §7.5:
                    selectUnreadCount, selectShouldShowSeparator,
                    selectShouldShowUnreadBadge,
                    selectShouldShowScrollToBottomButton,
                    selectShouldMarkAsRead. Plus convenience
                    selectUnreadMode / selectFirstUnreadMessageId /
                    selectFirstUnreadCreatedAt / selectLastReadAt.

Tests (all new __tests__/ files, 37 cases total):
- reducer.transitions.spec.ts (21 cases) — RV-4.1 / RV-4.7 / RV-4.8.
  All 7 event variants × clean/tracking/marked-unread, plus initial state
  invariants and exhaustiveness.
- selectors.spec.ts (12 cases) — RV-4.2..4.6 + RV-4.10.
  Each of the 5 visibility/decision selectors and the firstUnread* parity.
- parity.spec.ts (4 cases) — RV-4.9. Hand-rolled legacy 3-source
  simulator alongside the reducer for representative event sequences;
  asserts derived unreadCount + firstUnreadMessageId + firstUnreadCreatedAt
  match the legacy snapshot.

Backward compatibility
- BC-1 package.json public fields unchanged.
- BC-2 dist/types/index.d.ts export set unchanged (post `yarn build`).
- BC-3 context-shape-baseline.json present.
- BC-4 dist/types/index.d.ts does not re-export from internal/.
- BC-5 no external source imports from internal/ (module-private).
- BC-6 scrollPubSub topic contract unchanged.
- 0 existing test file modified.
- 0 production source modified — reducer is pure and unit-tested in
  isolation; consumer migration is post-cycle.

Stability
- 5 consecutive runs: 43 suite / 310 test deterministic (Phase 0+1+2+3+4).
- Full suite: 194/196 suites pass (2 pre-existing skipped, 0 fail).
- TypeScript clean (npx tsc --noEmit).
- ESLint clean.

Phase 4 mini-gate
  1. Behavior         ✓ existing tests unmodified + passing
  2. Characterization ✓ 15 Phase 0 specs unmodified + passing
  3. RV (Phase 4)     ✓ 37 cases covering all 7 events + 5 selectors
  4. BC-1..6          ✓ all PASS
  5. type / lint      ✓ clean

Branched from refactor/p0-phase-3-scroll-controller@cec85663.
…y flow

Restores the parallel-only invariant declared in Plan §2.4: a fault in the
runtime reducer, a mapper, or the underlying store layer MUST NOT prevent
the legacy GroupChannelProvider callback from continuing.

- `dispatchToRuntime` wraps the reducer + applyStorePatch chain in
  try/catch; on failure it returns [] and invokes the optional `onError`
  callback. `onError` is itself wrapped so its throw cannot propagate.
- `GroupChannelProvider` now dispatches through a `runtimeDispatch` thunk
  helper. The thunk form (`() => mapXxx(args)`) defers mapper execution
  into the same guard so a throw in a mapper is also contained. Logger
  surfaces both reducer- and mapper-side failures via `logger.warning`.
- New integration.spec.ts cases under "parallel-only invariant (W1)"
  use a malformed CHANNEL_READY event (`channel: null`) to provoke a
  real TypeError inside the reducer and assert: swallow + onError invoke
  + state preservation + no-onError safety.

Addresses W1 from G3 code review (.agentic/p0-impl/review/code-review.md).
@netlify
Copy link
Copy Markdown

netlify Bot commented May 27, 2026

Deploy Preview for sendbird-uikit-react ready!

Name Link
🔨 Latest commit 96ad27d
🔍 Latest deploy log https://app.netlify.com/projects/sendbird-uikit-react/deploys/6a16572c4326850008239d6f
😎 Deploy Preview https://deploy-preview-1428--sendbird-uikit-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96ad27dda4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hooks/useStore.ts
Comment on lines +103 to +105
if (memo.current.initialized && Object.is(memo.current.rawSnapshot, raw)) {
return memo.current.selected;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute selection when the selector changes

When a component passes a selector that closes over props (for example s => s.items[id]), changing id without a store update leaves the raw store snapshot reference unchanged, so this early return reuses the previous selection and never runs the new selector until some unrelated store notification happens. The existing useStore path recalculates through selectorRef.current, but this memoized path needs to invalidate when the selector/equality function changes or include that identity in the cache key.

Useful? React with 👍 / 👎.

Comment on lines +135 to +136
const recordAndRun = async (intent: ScrollIntent): Promise<void> => {
log.push(intent);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid retaining every viewport scroll intent

useMessageListScroll now attaches attachViewportObserver in production, and each window resize/orientation/visualViewport scroll can reach notifyViewportChanged, which calls this path and appends to log. Because the log is only a test/debug affordance and is never capped or cleared during a channel session, mobile keyboard viewport events or repeated resizes grow this array indefinitely even though the default executor is a no-op; gate the log to non-production or bound it to the last intent(s).

Useful? React with 👍 / 👎.

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