refactor(group-channel): P0 runtime stability scaffolding (Phase 0-4)#1428
refactor(group-channel): P0 runtime stability scaffolding (Phase 0-4)#1428danney-chun wants to merge 8 commits into
Conversation
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).
✅ Deploy Preview for sendbird-uikit-react ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
💡 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".
| if (memo.current.initialized && Object.is(memo.current.rawSnapshot, raw)) { | ||
| return memo.current.selected; | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
| const recordAndRun = async (intent: ScrollIntent): Promise<void> => { | ||
| log.push(intent); |
There was a problem hiding this comment.
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 👍 / 👎.
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 theinternal/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.
.agentic/refactor-design.ko.md§5.8 / §21.agentic/p0-impl/{spec,plan,notes}.md,.agentic/p0-impl/review/code-review.mdPhase-by-phase (8 commits)
9b2fb628d050c9bauseStoreSelector(narrow-slice subscription,useSyncExternalStore+ memoizing getSnapshot) +applyStorePatch(dev-instrumented patch helper)fe098a52mapChannelReady,mapOnMessagesReceived, etc.)2e0a1176GroupChannelProvider— parallel-only / additive; no legacy state replaced05d4cc8dScrollIntent+ScrollAnchor+ScrollController(pure, no-op executor default)5a48f14fuseMessageListScrollsubscribes scrollPubSub topics → typed intents (controller side-by-side with legacy DOM handlers)e78f1f2fUnreadReducer+ selectors with parity tests against legacyuseUnreadcomputation96ad27dddispatchToRuntimefailures from legacy flow (parallel-only invariant)BC invariants (all PASS —
./scripts/bc-check.sh)package.jsonpublic fields (name/version/main/module/types/exports/peerDependencies/dependencies) — unchangeddist/types/index.d.tsexport set — unchangeduseGroupChannelContext()field set) — well-formeddist/types/index.d.tsdoes NOT re-export anything frominternal/internal/(module-private boundary intact)scrollPubSubtopic + payload set + call sites — unchangedParallel-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:dispatchToRuntimewraps the reducer +applyStorePatchchain in try/catch; on failure it returns[]and invokes the optionalonError(logger).onErroris itself wrapped.GroupChannelProvider'sruntimeDispatchthunk 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):channel: null) → reducer throws → swallow + onError invokedTest plan
./scripts/bc-check.sh— BC-1 ~ BC-6 all PASS post-rebaseyarn jest src/modules/GroupChannel/internal/ src/utils/test/p0/— 13 suites / 136 tests PASSyarn jest(pre-rebase, source identical) — 194/196 PASS (2 pre-existing skipped, 0 fail), 1112/1122 + 10 skippedyarn buildclean (pre-existing warnings only — named/default export mix, ContextMenu circular deps)Out of scope (intentional — follow-up cycles)
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.as nevercast removal onmapOnMessagesReceived/mapOnMessagesUpdated— widen mapper input type toreadonly BaseMessage[]so the cast is unnecessaryuseStoreSelectorJSDoc: explicit guidance that consumers must pass module-level oruseCallback-stabilized selectorsrecordAndRunrejection guard + reference-equality fast path inhasStateChangedbeforeSCROLL_POSITION_CHANGEDhigh-frequency dispatch beginsonCacheResult/onApiResultwiring: pending@sendbird/uikit-toolsversion bump (callbacks exist in local coreTs source but not in published0.1.0)Reviewer guide
src/modules/GroupChannel/internal/and are NOT in the package barrel.useStore.ts(additive exports),storeManager.ts(additive exports),GroupChannelProvider.tsx(additive dispatch sites +runtimeDispatchhelper),useMessageListScroll.tsx(additive controller setup).importline inuseMessageListScroll.tsxthat was expanded to adduseEffect.🤖 Generated with Claude Code