feat!: adopt stream-chat's branded TimestampNS - #3297
Conversation
stream-chat now types every server-sent date as `TimestampNS` and makes `new Date(timestamp)` a compile error (GetStream/stream-chat-js#1884). - `processMessages` `lastRead`, `MessageList` `headerPosition` / `insertIntro` and the `VirtualizedMessageList` `lastReadDate` render prop are typed `TimestampNS` - mock builders mint branded timestamps (`convertDateToTimestamp` returns `TimestampNS`) - tests brand epoch/derived literals with `asTimestampNS`; a test that passed a `Date` as `lastRead` now passes a wire timestamp, and a test no longer imports the sibling `stream-chat-js/src` checkout - the Vite example's WebSocket event templates carry `TimestampNS` - migration docs describe the brand, the Date guard and its gaps, and drop the stale `latestMessageDatesByChannels` references BREAKING CHANGE: `processMessages`' `lastRead`, `MessageList`'s `headerPosition` and the `lastReadDate` render prop are typed `TimestampNS`; a plain millisecond `number` no longer compiles.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…guard (#1884) ## CLA - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [x] Code changes are tested ## Description of the changes, What, Why and How? Follow-up to GetStream/chat#17356 ([REACT-1179](https://linear.app/stream/issue/REACT-1179)), which taught the TypeScript generator to type every server-sent date as `TimestampNS`, a branded unix-nanosecond `number`, and to emit `models/timestamp-guard.d.ts`, which makes `new Date(timestamp)` a compile error. This PR regenerates the client with it and closes the gaps the brand exposed. **Why:** a nanosecond timestamp is out of `Date`'s range, so the natural `new Date(message.created_at)` silently yields an Invalid Date that throws later from `.toISOString()` or renders "Invalid Date". Until now nothing flagged it at compile time. ### Time helpers (`src/utils/time.ts`) - `nowNs`, `msToNs` and `dateToNs` return `TimestampNS`. - `nsToDate`, `nsToRfc3339` and `convertTimestampToDate` require a `TimestampNS`, so `nsToDate(Date.now())` no longer compiles instead of returning a 1970 date. - `nsToMs` keeps taking a plain `number`, because it also converts durations (the difference of two timestamps). - New `asTimestampNS(n)` brands a number that is already in nanoseconds (DB rows, fixtures, the epoch `asTimestampNS(0)`). It is exported from the root. ### Hand-written types carry the brand Plain `number` would drop the brand, so `new Date(channel.state.read[id].last_read)` would still compile. These are now `TimestampNS`: - read state (`last_read`, `last_delivered_at`), mute status, `lastRead()` / `countUnread()` - thread state, poll `lastActivityAt`, reminder state and `timeLeftMs` - the receipts tracker and delivery reporter - the paginators (`lastMessageAt`, unread snapshot, `truncate`, deletion, `findItemByTimestamp`) - the `LocalEvent` / `ConnectedEvent` timestamps, the cooldown timer, the composer audit clock, and offline `truncated_at` ### Shipping the guard to consumers The client is regenerated from GetStream/chat master, which since GetStream/chat#17460 emits the guard as `models/timestamp-guard.ts`, a module `tsc` compiles into `dist/types` like any other file. No copy step is needed. Emitting it is not enough on its own, though: a global augmentation only loads for a consumer if the entry point's type graph imports it. So: - `src/index.ts` re-exports the guard type-only (`export type {} from './gen/models/timestamp-guard'`). Declaration emit keeps it, and esbuild erases it, so there is no runtime import of an empty module. Without this line the guard is still emitted but never loads for a consumer; I checked, and the dist type check below then fails. - `test/types/timestamps.ts` holds compile-time assertions, run twice: - against `src` by `yarn types`, which **this PR adds to PR CI**; - against the built `dist`, resolved through `exports` exactly as a consumer would, at the end of `yarn build`. A release whose published types lost the guard now fails. ### Other timestamp fixes found while auditing - **`pinMessage`**: a `number` there means seconds, so passing `message.pinned_at` compiled and threw at runtime. Both parameters now reject a `TimestampNS`. - **Offline read state**: `handleRead` persisted `received_at` (the local clock at receipt) as `last_read`. For a mark-unread that is "now", which is past the messages just marked unread, so a cold start from SQLite lost the unread boundary. It now writes what `Channel` writes: `last_read_at` for a mark-unread, `created_at` otherwise. - **`rate_limit_reset`**: `new Date('1700000000')` is an Invalid Date. The header is now read as unix seconds. - The dev burst simulator and several stale test fixtures used ISO strings or `Date`s; they now use nanoseconds. ### Unrelated items in the regen These come from the current spec, not from the timestamp work: - `translateMessage` now returns `TranslateMessageResponse`. - `updatePoll` forwards `team`. - `appeal` forwards `channel_cid`. The generator's compact-interface change (#17356) also makes the `src/gen/models/index.ts` diff mostly whitespace. ### Verification - `yarn types` (src, scripts, type assertions), `yarn lint`, `yarn test` (3967 passing) and `yarn build` (including the dist type check) all pass after rebasing on `release-v10`. - I checked that the assertions aren't hollow: against a stale build, the `pinMessage` assertions fail with "Unused `@ts-expect-error`". - I ran stream-chat-react's Vite example and stream-chat-react-native's SampleApp (iOS simulator) against this build. The channel list, message list, date separators, threads, drafts and search all render correct dates. The RN offline database holds integer nanoseconds and hydrates correctly on a cold start. Related PRs: - GetStream/stream-chat-react#3297 and GetStream/stream-chat-react-native#3822 adopt this. Both need a stream-chat release that includes it. - GetStream/chat#17425 lists `asTimestampNS` in the generator template; this PR's generated file already carries the same line. ## Changelog - **BREAKING:** server-sent timestamps are typed `TimestampNS` (a branded `number`). Constructing a response-shaped object needs `nowNs()` / `msToNs()` / `dateToNs()` / `asTimestampNS()`, and `new Date(timestamp)` no longer compiles. - **BREAKING:** `nsToDate`, `nsToRfc3339` and `convertTimestampToDate` require a `TimestampNS`; `nowNs`, `msToNs` and `dateToNs` return one. - **BREAKING:** `client.pinMessage` rejects a server timestamp where a number means a seconds offset. - New `asTimestampNS` helper. - Fix: offline read state persists the server read timestamp instead of the local receipt time. - Fix: `rate_limit_reset` is parsed from unix seconds instead of producing an Invalid Date.
## [10.0.0-rc.14](v10.0.0-rc.13...v10.0.0-rc.14) (2026-09-24) ### ⚠ BREAKING CHANGES * brand server-sent timestamps as TimestampNS and ship the Date guard (#1884) ### Features * brand server-sent timestamps as TimestampNS and ship the Date guard ([#1884](#1884)) ([e51198a](e51198a)), closes [GetStream/chat#17356](https://github.com/GetStream/chat/issues/17356) [GetStream/chat#17460](https://github.com/GetStream/chat/issues/17460) [#17356](https://github.com/GetStream/stream-chat-js/issues/17356) [GetStream/stream-chat-react#3297](GetStream/stream-chat-react#3297) [GetStream/stream-chat-react-native#3822](GetStream/stream-chat-react-native#3822) [GetStream/chat#17425](https://github.com/GetStream/chat/issues/17425)
rc.14 is the first release with TimestampNS, asTimestampNS and the published Date guard, which this SDK now imports. Refs: GetStream/stream-chat-js#1884
|
Size Change: -235 B (-0.03%) Total Size: 845 kB 📦 View Changed
ℹ️ View Unchanged
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## release-v15 #3297 +/- ##
==============================================
Coverage ? 85.42%
==============================================
Files ? 529
Lines ? 15568
Branches ? 4913
==============================================
Hits ? 13299
Misses ? 2269
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ents stream-chat 10.0.0-rc.14 types `TypingStartEvent.member` as `ChannelMemberPartialResponse`, which the Vite example's `EventPayload` rejected, failing `tsc` in the example build (and both Vercel deploys).
🎯 Goal
Adopt stream-chat's branded
TimestampNS(GetStream/stream-chat-js#1884, REACT-1179). stream-chat now types every server-sent date asTimestampNS, a branded unix-nanosecondnumber. Its published types also makenew Date(timestamp)a compile error, because a nanosecond value is out ofDate's range and silently becomes an Invalid Date.An audit of this SDK found no runtime unit bugs: every server timestamp that becomes a
Date, gets formatted, or meetsDate.now()already goes throughconvertTimestampToDate/nsToDate/nsToMs. The work is closing the type gaps so the brand, and with it the guard, reaches the public props and the tests.🛠 Implementation details
Public types (breaking)
These now take
TimestampNS, so a millisecondnumberno longer compiles:ProcessMessagesParams.lastRead(processMessages)MessageListheaderPosition/insertIntro. It already had to be in nanoseconds, but was typednumber.VirtualizedMessageListlastReadDaterender propChannelFilesView.utils's internalnormalizeTimestampTests and fixtures
mock-builders/generator/time.tsconvertDateToTimestampreturnsTimestampNS, which makes the generated fixtures assignable (about 50 test errors fixed in one place).0,NaN,now - msToNs(…)) go throughasTimestampNS.MessageList/__tests__/utils.test.tspassed aDateaslastReadthrough an untyped helper; it now passesnowNs(), and the helper is typed.NotificationAnnouncer.test.tsximported a type from the sibling../stream-chat-js/srccheckout; it now imports fromstream-chat. That removes 17 errors fromyarn types:tests.Other
TimestampNS.ai-docs/ai-migration-v14-v15.mdnow documents:ts ?? Date.now(),Math.max(…), and unit mix-ups;asTimestampNSand the updated type table.latestMessageDatesByChannelsas retyped; it now points to the section that records its removal.i18n-v15-migration.mdno longer destructures it fromuseChat.ChatContextValue.mutesis fixed.Verification (against the published
stream-chat@10.0.0-rc.14, after merging the latestrelease-v15)yarn types: clean. The unrelated errors this PR described earlier (RetrySendMessageWithLocalUpdateParams, the notification translators) were fixed onrelease-v15by feat(Attachment): render live upload progress on messages being sent #3295 / fix(MessageList): show delivery and read state on every message that has it #3296.yarn test: 2840 passing, 252 files. TheChannel.test.tsxfailure is gone too.yarn lintandyarn validate-translations: clean.message.newall render correct dates, with no "Invalid Date" and no console errors.🎨 UI Changes
None. Types, tests and docs only.