feat!: adopt stream-chat's branded TimestampNS - #3822
Merged
Merged
Conversation
stream-chat now types every server-sent date as `TimestampNS` and makes `new Date(timestamp)` a compile error (GetStream/stream-chat-js#1884). - offline DB read mappers brand stored integers: `mapStorableToTimestamp` returns `TimestampNS`, and the new `mapStorableToRequiredTimestamp` replaces the `?? 0` fallbacks - `findInMessagesByDate`, `getChannelUnreadState`, `deleteMessagesForChannel` and `useIsChannelMuted` (now core's `ChannelMuteStatus`) carry `TimestampNS` - mock builders mint branded timestamps; tests brand epoch/derived literals, and the reaction-group fixtures use nanoseconds instead of ISO strings - SampleApp / ExpoMessaging timestamp props and event templates carry `TimestampNS` - remove the write-only `lastReadRef` in `Channel` and the unused `utils/date.ts` - migration doc describes the brand, the Date guard and its gaps BREAKING CHANGE: `findInMessagesByDate` takes a `TimestampNS`, and `getChannelUnreadState` / `useIsChannelMuted` return `TimestampNS` timestamps.
1 of 2 tasks
oliverlaz
requested review from
isekovanic and
szuperaz
and removed request for
szuperaz
September 23, 2026 14:07
szuperaz
approved these changes
Sep 23, 2026
oliverlaz
added a commit
to GetStream/stream-chat-js
that referenced
this pull request
Sep 24, 2026
…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.
github-actions Bot
pushed a commit
to GetStream/stream-chat-js
that referenced
this pull request
Sep 24, 2026
## [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
Contributor
SDK Size
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 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, the offline store and the example apps found no runtime unit bugs: every server timestamp that becomes a
Dateor meets the local clock already goes throughconvertTimestampToDate/nsToDate/nsToMs/nowNs. The work is making values from SQLite, fixtures and example code assignable to the branded types.🛠 Implementation details
Offline DB
mapStorableToTimestampreturnsTimestampNS | undefined.mapStorableToRequiredTimestampreplaces the 17mapStorableToTimestamp(x) ?? 0fallbacks across the 10 read mappers, with unchanged behaviour: an absent column still reads back as the epoch.JSON.parse(extraData), which isany; all of them are updated.schema.tsstaynumber, since they are the storage boundary.Public API (breaking)
findInMessagesByDate(messages, targetTimestamp)takes aTimestampNS(its stale JSDoc is fixed too).getChannelUnreadStatereturnslast_readasTimestampNS, falling back toasTimestampNS(0).useIsChannelMuteduses core'sChannelMuteStatusinstead of a hand-copied shape.deleteMessagesForChannel'struncated_atis aTimestampNS.Tests and fixtures
mock-builders/generator/time.tsconvertDateToTimestampreturnsTimestampNS.asTimestampNS.first_reaction_at/last_reaction_at, which is the wrong unit; they now usenowNs().Example apps
DraftsList'sdateand the WebSocket event templates'created_atareTimestampNS.end_atit reads from the route.Cleanup
lastReadRefinChannel.src/utils/date.ts(secondsUntil).Docs:
ai-docs/ai-migration-v9-to-v10.mdnow documents:ts ?? Date.now(),Math.max(…), and unit mix-ups;asTimestampNS, the updated signatures, and how the offline mappers brand rows.Verification (first against a local build of GetStream/stream-chat-js#1884, re-run against the published
stream-chat@10.0.0-rc.14with the same results)cd package && yarn typecheck(source, tests and mock builders): no new errors. The two remaining errors predate this PR and also appear on plainV10against the same build:useChannelRequestHandlersfrom stream-chat-js#1882's message-operation API change;notificationTranslationslacks the newapi:message:send:failed/update:failedtypes (Android - The pictures (available for the attachment) are disappeared suddenly. #1881).yarn test:unit: 2000 tests passing (203 suites).🎨 UI Changes
None. Types, offline-mapper typing, tests and docs only; rendering is unchanged.
🧪 Testing
cd package && yarn typecheck && yarn test:unit. For end-to-end checks, run SampleApp against a stream-chat build that includes GetStream/stream-chat-js#1884.