Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
328c7c2
feat: support multiple channel lists with ChannelPaginatorsOrchestrator
MartinCupela Sep 3, 2025
65fd6cd
feat: allow to boost paginator items and lock item order
MartinCupela Sep 15, 2025
69b421f
feat: support missing ChannelManager features in BasePaginator
MartinCupela Nov 7, 2025
f4892c6
feat: add filter resolvers for channel filters archived, app_banned, …
MartinCupela Nov 10, 2025
aeaa92d
feat: allow to keep channels in certain matching paginators and not i…
MartinCupela Nov 10, 2025
8938419
feat: allow to keep channels in certain matching paginators and not i…
MartinCupela Nov 11, 2025
03614c7
fix: do not remove channel from paginator on channel.hidden
MartinCupela Nov 11, 2025
0187696
chore: move paginators to a dedicated folder
MartinCupela Nov 18, 2025
53941ef
feat: introduce intervals to BasePaginator
MartinCupela Nov 21, 2025
7aa4f91
feat: add MessagePaginator
MartinCupela Jan 12, 2026
98c4d1b
chore(Thread): add initial spec for Thread Constructor Minimal Init
MartinCupela Feb 27, 2026
1ff2271
feat(Thread): implement Add Optional `threadData` Constructor Branch …
MartinCupela Feb 27, 2026
9650862
feat(Thread): implement Complete Hydration + Pagination Bootstrap for…
MartinCupela Feb 27, 2026
2ab25b9
feat: add reactive states to ChannelState
MartinCupela Mar 3, 2026
97c2650
Merge remote-tracking branch 'wt/agent/feat/init-empty-thread' into f…
MartinCupela Mar 3, 2026
892f6e6
test: fix message composer tests
MartinCupela Mar 3, 2026
3a7c938
feat: add message send retry cache
MartinCupela Mar 3, 2026
9172f11
feat: query replies with MessagePaginator
MartinCupela Mar 4, 2026
a703f7c
feat: support delete operation on MessageOperations
MartinCupela Mar 4, 2026
314ea2c
feat: allow to define custom mark-read request function for Thread an…
MartinCupela Mar 4, 2026
fe7a68d
feat: add messageFocusSignal state to MessagePaginator
MartinCupela Mar 4, 2026
cf4252e
refactor: remove thread from Channel's CustomMarkReadRequestFn
MartinCupela Mar 4, 2026
aa706de
test: fix failing tests
MartinCupela Mar 5, 2026
1fbf420
feat: export configuration service
MartinCupela Mar 5, 2026
a3f4bd7
feat: add backwards compatible APIs
MartinCupela Mar 5, 2026
ed0ce71
fix: nullify first_unread_message_id on message.read event
MartinCupela Mar 5, 2026
c972ee9
fix: update thread participant counts and reply counts on message.new…
MartinCupela Mar 5, 2026
43d365f
feat: decouple request sort from in-memory item order via BasePaginat…
MartinCupela Mar 5, 2026
2444199
fix: make Channel's MessagePaginator ingest message on message.new
MartinCupela Mar 5, 2026
a971056
fix: make Channel's MessagePaginator react to channel.truncated, user…
MartinCupela Mar 5, 2026
08d2073
fix: reflect quoted message update among MessagePaginator items and r…
MartinCupela Mar 5, 2026
31eedab
fix: emit new paginator state always when jumping to a message
MartinCupela Mar 6, 2026
b58912f
merge: integrate PR #1674 (paginator stack) into release-v10
MartinCupela Jul 3, 2026
02608e3
test/fix: green up PR #1674 merge (types, lint, 3467 tests pass)
MartinCupela Jul 3, 2026
19489c0
feat: add UserGroupPaginator.ts
MartinCupela Jul 8, 2026
fe9b398
Merge remote-tracking branch 'origin/master' into feat/message-pagina…
isekovanic Jul 8, 2026
70d34ab
fix: pagination cursors for offline support
isekovanic Jul 9, 2026
9f4d3f1
feat(ChannelPaginatorOrchestrator): add ingestChannel method
MartinCupela Jul 13, 2026
634a90c
fix(filterCompiler): prevent short-circuiting logical operators
MartinCupela Jul 13, 2026
3f12e0c
feat(ChannelPaginator): add mutedFilterResolver
MartinCupela Jul 13, 2026
fc273e0
feat(MessagePaginator): add scheduleMessageFocusSignalClear method
MartinCupela Jul 13, 2026
c46639d
Merge remote-tracking branch 'origin/feat/message-paginator-master-me…
MartinCupela Jul 14, 2026
179944b
fix: message paginator fixes and extra features (#1802)
isekovanic Jul 16, 2026
720519d
fix: mark read live state freezing (#1803)
isekovanic Jul 17, 2026
7f0506c
fix: clean up thread state remnants (#1804)
isekovanic Jul 20, 2026
3d2a56a
refactor: replace legacy ChannelState message/thread/pinned storage w…
MartinCupela Jul 22, 2026
896f499
feat(pagination): add intervalViews store (#1807)
MartinCupela Jul 24, 2026
1acbefb
refactor: merge OpenAPI changes (#1809)
MartinCupela Jul 29, 2026
a7f0ce9
Merge branch 'release-v10' of github.com:GetStream/stream-chat-js int…
arnautov-anton Jul 30, 2026
d00e67f
Post-merge fixes
arnautov-anton Jul 30, 2026
a6d8a7f
Some more test fixes
arnautov-anton Jul 31, 2026
e775a26
fix: offline db channel order openapi regression
isekovanic Aug 1, 2026
9ad7f1a
test: fix offline db channel order openapi regression
MartinCupela Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ This is a single-package SDK with **no monorepo**. The public surface is everyth
### Module map of `src/`

- **`client.ts` — `StreamChat` facade.** ~5k-line class. Prefer `StreamChat.getInstance(key, secret?, options?)` — the constructor exists for advanced uses but `getInstance` is what `connectUser` warnings and most docs assume. Owns: the axios instance, WS connection lifecycle, `TokenManager`, and a registry of subsystem managers (`threads`, `polls`, `notifications`, `reminders`, `moderation`, `uploadManager`, `messageDeliveryReporter`, plus an optional `offlineDb` injected via `setOfflineDBApi`). New REST endpoints are added here as methods that call `axiosInstance` and return a type from `types.ts`.
- **`channel.ts` (~2.5k lines) + `channel_state.ts` (~1.1k) + `channel_manager.ts` + `channel_batch_updater.ts`** — per-channel object, its in-memory state, and the manager that orchestrates collections of channels (query/sort/filter, pagination, archived/pinned handling).
- **`channel.ts` (~2.5k lines) + `channel_state.ts` (~1.1k) + `channel_manager.ts` + `channel_batch_updater.ts`** — per-channel object, its in-memory state, and the manager that orchestrates collections of channels (query/sort/filter, pagination, archived/pinned handling). **Messages are NOT stored on `channel.state`.** The message list, thread replies, and pinned messages each live in a paginator — `channel.messagePaginator`, `thread.messagePaginator`, and `channel.pinnedMessagesPaginator` — which are the single source of truth (interval storage + a canonical `ItemIndex`). Read them via `channel.messagePaginator.state.items` / `.getItem(id)` / `.headmostItem` (newest loaded item), and mutate via the paginator (`ingestItem` / `removeItem`), never a legacy `channel.state.addMessageSorted()` / `state.messages` (removed). `channel.state.last_message_at` was **removed**; the channel's latest-message timestamp lives on `channel.messagePaginator.lastMessageAt` (its `aggregateState` store — seeded from `ChannelResponse.last_message_at`, then advanced monotonically as messages are ingested). See `docs/breaking-changes-v14-v15.md`.
- **`connection.ts` (`StableWSConnection`) + `connection_fallback.ts` (`WSConnectionFallback`)** — realtime transport. Primary WS implementation does its own 25s ping / 35s health-check loop and reconnects on close/error/offline events; the fallback long-polls over HTTP. The client picks between them based on first-connect outcome; both emit `connection.changed` / `transport.changed` events into the client's local event bus.
- **`store.ts` — `StateStore`.** Reactive primitive (see "State and subscription patterns" below).
- **`signing.ts` — webhook + token helpers.** Server-side primitives `verifyAndParseWebhook`, `parseSqs`, `parseSns`, `verifySignature` (recent CHA-3071 added compressed-payload support). These are re-exported through `client.ts`. **The HMAC is always computed over the uncompressed JSON bytes** — gzip detection uses the `1f 8b` magic bytes, not headers, so the same handler works whether your platform middleware auto-decompressed or not. `CheckSignature` is deprecated in favor of `verifySignature` purely to fix parameter order; new code should use `verifySignature(body, signature, secret)`.
Expand Down
164 changes: 164 additions & 0 deletions docs/breaking-changes-v14-v15.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Breaking Changes: v14 → v15 (stream-chat JS)

Consumer-facing **breaking changes** on the v15 line of the JS SDK — the `message-paginator`
initiative (`feat/message-paginator-master-merge` and the follow-on
`refactor/remove-legacy-channelstate-storage`), beyond the master ⇄ PR merge itself. This is the
source for the v14 → v15 migration guide / release notes. Append newest at the top. Keep each entry
self-contained: what changed, before → after, how to migrate, why.

> Scope note: this file tracks **public API / observable behavior** changes. Internal refactors with
> identical output belong in `decisions.md`, not here.

---

## `ChannelState` message / thread / pinned storage removed — paginators are the source of truth

**Area:** `ChannelState`, `Channel`, `utils` · **Status:** implemented

The channel's messages, thread replies, and pinned messages are no longer stored on `channel.state`.
Each list now lives in a paginator that is the single source of truth (interval storage + a canonical
`ItemIndex`):

- **Main message list** → `channel.messagePaginator`
- **Thread replies** → `thread.messagePaginator` (via `client.threads` / the `Thread` object)
- **Pinned messages** → `channel.pinnedMessagesPaginator`

### Removed from `ChannelState`

Properties / getters: `messages`, `latestMessages`, `messageSets`, `messagePagination`, `threads`,
`pinnedMessages`. (Also `isUpToDate` and the `last_message_at` setter — see the dedicated entries
below.)

Methods: `addMessageSorted`, `addMessagesSorted`, `removeMessage`, `findMessage`,
`findMessageByTimestamp`, `filterErrorMessages`, `loadMessageIntoState`, `clearMessages`,
`initMessages`, `pruneOldest`, `addReaction`, `removeReaction`, `updateUserMessages`,
`deleteUserMessages`, `addPinnedMessages`, `addPinnedMessage`, `removePinnedMessage`,
`removeQuotedMessageReferences` (plus the internal `_updateMessage` / `_updateQuotedMessageReferences`
/ `_add*`/`_remove*` reaction helpers).

### Removed from `utils` (re-exported through the package root)

`addToMessageList`, `messageSetPagination`, `binarySearchByDateEqualOrNearestGreater`,
`deleteUserMessages`, and the `MessageSet` / message-set pagination types.

### Migrate

| Before (v14) | After (v15) |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `channel.state.messages` | `channel.messagePaginator.state.items` (reactive) / `channel.messagePaginator.items` |
| `channel.state.latestMessages` | `channel.messagePaginator.headItems` / `.lastMessage` |
| `channel.state.messagePagination` | `channel.messagePaginator.state` (`hasMoreHead` / `hasMoreTail` / `cursor`) |
| `channel.state.threads[parentId]` | `thread.messagePaginator.state.items` (resolve the `Thread` via `client.threads`) |
| `channel.state.pinnedMessages` | `channel.pinnedMessagesPaginator.state.items` |
| `channel.state.addMessageSorted(m)` / `addMessagesSorted(ms)` | `channel.messagePaginator.ingestItem(m)` |
| `channel.state.removeMessage({ id })` | `channel.messagePaginator.removeItem({ id })` |
| `channel.state.findMessage(id)` | `channel.messagePaginator.getItem(id)` |

Reactive reads use `useStateStore(channel.messagePaginator.state, …)` (or the paginator's `state`
store directly). Pin/unpin, reactions, user updates, and deletions are applied to the paginators by
the SDK's own event handlers — application code should not call the removed mutators.

### Why

Messages were previously stored twice — a flat `ChannelState` list plus message-set/pagination
bookkeeping — which had to be kept in sync with the paginators and could drift. Consolidating on the
paginators removes the dual-write, gives one API/behavior across the main list, threads, and pinned
messages (dedup-by-id, interval merge, `getItem`/`removeItem`, head-window semantics), and lets
`last_message_at` be derived rather than separately maintained.

## `ChannelState.last_message_at` removed — use `channel.messagePaginator.lastMessageAt`

**Area:** `ChannelState` · **Status:** implemented

`channel.state.last_message_at` was **removed entirely** (it was briefly a read-only getter earlier
in v15; that getter is gone too). The channel's latest-message timestamp is now owned by the message
paginator as a whole-collection aggregate.

- **Before:** `channel.state.last_message_at` (writable, then a derived getter). Internally
maintained by the now-removed `Channel._trackLatestMessage`.
- **After:** `channel.messagePaginator.lastMessageAt` — a `Date | null` **derived** getter over the
paginator's `aggregateState` store (`MessagePaginatorAggregateState = { lastMessage,
seededLastMessageAt }`). It returns `max(lastMessage?.created_at, seededLastMessageAt)`:
`lastMessage` is the newest loaded/received message (advanced on ingest), `seededLastMessageAt` is
the server floor **seeded from `ChannelResponse.last_message_at`** (for channels whose newest
message isn't loaded). Deriving the sort key from the two independent facts means it can never drift
from the display message. Subscribe to `channel.messagePaginator.aggregateState` for reactivity.
- **Migrate:** replace `channel.state.last_message_at` reads with
`channel.messagePaginator.lastMessageAt`. It is not writable; the value is derived from ingested
messages and the server seed.
- **Why:** the message paginator is the single source of truth for messages; `last_message_at` is an
aggregate over them (the dual of pagination). Deriving it through a `ChannelState` getter that
reached into the paginator's message index risked stale/mixed-basis sorting (a seeded-but-stale
paginator preferred over a fresher server value); a single seeded-then-advanced value on the
paginator removes that hazard.
- **Tracking relocated:** `MessageIntervalPaginator`'s `state.latestMessageId` and the `latestMessage`
getter (id resolved from the pagination `state`) were replaced. The tracked latest now lives on
`MessagePaginator.aggregateState.lastMessage`, advanced on every ingest.
`MessagePaginator.lastMessage` remains as a convenience getter but now reads `aggregateState`.
This matters for reactivity: pagination `state` only emits when the **active** interval is impacted,
so a WS message landing in the (non-active) head interval would not notify a `state`-derived
latest; `aggregateState` is written directly on each advance and emits regardless — subscribe to it
(e.g. for a channel/thread list item's latest-message display). `aggregateState.lastMessage` is a
LIVE reference: refreshed in place on edit/soft-delete/reaction of the current latest and recomputed
on hard-remove, and it honors `skip_last_msg_update_for_system_msgs` (system messages neither
reorder a channel nor become its displayed latest). A consumer that previously showed the unfiltered
newest message (`headmostItem`) as the channel-list preview will now skip system messages under that
config — a deliberate behavior change so the preview and the channel's sort position agree.

## Newest-loaded window is exposed as computed getters `headItems` / `headmostItem`

**Area:** `BasePaginator` · **Status:** implemented

The newest-loaded window is exposed as **computed getters** on the paginator — `paginator.headItems`
(the window; `[]` before the first load) and `paginator.headmostItem` (its single newest item),
derived from the intervals on read.

- **Migrate:** the v14 `channel.state.latestMessages` reactive array becomes
`channel.messagePaginator.headItems`. Because these are getters, **not** `PaginatorState` fields,
they are not subscribable via `useStateStore(paginator.state, (s) => s.headItems)` — read them
directly, or subscribe to `channel.messagePaginator.aggregateState` for the reactive last-message
signal.
- **Why:** the value is derivable from the intervals on demand, so it does not need to be materialized
(and re-emitted) into pagination state.

## `Channel.lastMessage()` removed — use `channel.messagePaginator.headmostItem`

**Area:** `Channel` · **Status:** implemented

`channel.lastMessage()` was removed. It returned the newest loaded message (the head edge of the
message paginator's latest window); read `channel.messagePaginator.headmostItem` directly instead.

- **Migrate:** `channel.lastMessage()` → `channel.messagePaginator.headmostItem`.
- **Note:** `headmostItem` is the newest _loaded_ message, **unfiltered** (includes system messages) —
distinct from `channel.messagePaginator.lastMessage`, the filtered chronological latest (honors
`skip_last_msg_update_for_system_msgs`) that backs `lastMessageAt`. Use `headmostItem` for "the newest
message on screen"; use `lastMessage` / `lastMessageAt` for channel-list ordering.
- **Why:** it was a thin wrapper over `headmostItem`, and sharing the name `lastMessage` with the
differently-filtered paginator getter was misleading.

## `ChannelState.isUpToDate` / `setIsUpToDate` removed

**Area:** `ChannelState` · **Status:** implemented

The `isUpToDate` flag and its `setIsUpToDate(boolean)` setter were removed. They gated whether an
incoming `message.new` was appended to the visible message list when the user had scrolled to older
history.

- **Before:** UI SDKs set `channel.state.setIsUpToDate(false)` when jumping to an older window so live
messages were not forced onto the visible list, and read `channel.state.isUpToDate` to decide
whether to show a "jump to latest" affordance.
- **After:** neither exists. Message routing is handled structurally by the message paginator: a live
message newer than the loaded head lands in the head (or logical-head) interval, which is not the
active window when the viewer has jumped away, so the visible window is preserved with no flag.
- **Migrate:**
- "Am I viewing the newest window?" → `channel.messagePaginator.isActiveIntervalAtHead` (getter).
- "Are there newer messages not yet loaded?" → `channel.messagePaginator.hasMoreHead` (reactive
via `channel.messagePaginator.state`).
- "Jump to the latest" → `channel.messagePaginator.jumpToTheLatestMessage()`.
- **Behavioral note:** `last_message_at` now advances on every incoming message regardless of the
viewer's scroll position (the old `isUpToDate` suppression is gone) — it is a channel-level fact,
independent of what the UI is currently viewing.
- **Why:** the flag duplicated state the paginator already models, and was only ever set `false` on
disconnect on this branch — its message-list responsibility had already moved to the paginator.

---
Loading