Repository navigation
feat: support scheduled messages (CHA-5934) - #1915
Open
nijeesh-stream wants to merge 3 commits into
Open
nijeesh-stream wants to merge 3 commits into
nijeesh-stream wants to merge 3 commits into
Conversation
Add scheduled_at to sendMessage options, the update/delete/send_now/query scheduled message endpoints, the scheduled_message.* events and a ScheduledMessageManager (client.scheduledMessages) that keeps the current user's scheduled messages in sync from the query, channel queries with include_scheduled_messages, and events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nijeesh-stream
requested review from
MartinCupela,
isekovanic,
oliverlaz and
szuperaz
as code owners
October 14, 2026 22:16
Messages awaiting a send retry report status sending, pending: false is accepted with scheduled_at, and the scheduled send response hydrates mentioned_users. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ignore an upsert older (by updated_at) than the stored version, so the send-now response or its updated event cannot revert a failed message to scheduled. - load() and channel query hydration no longer undo changes that arrived while they were in flight. - Drop responses to requests made before disconnectUser(), so a new user never sees the previous user's scheduled messages. - getForChannel accepts parentId: null for messages outside threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Note
The backend side (CHA-5924) is merged: edit, delete, send-now, query and
include_scheduled_messages(GetStream/chat #18597, #18655, #18656) shipped in chatv239.72.0. Scheduling stays behind a per-app flag that is off by default. Check that the target shards runv239.72.0or later before relying on this in production.TLDR
channel.sendMessage(message, { scheduled_at })schedules a message. Four new client methods call the new endpoints. Thescheduled_message.*events are typed. A newclient.scheduledMessagesstore, built likeclient.reminders, holds the current user's scheduled messages. It loads up to 100 with one query, then stays in sync from events: created and updated messages are upserted, failed ones are markedfailed, and sent or deleted ones are removed. It reloads after a connection recovery.scheduled_atskips the offline queue, because replaying a queued scheduled send would add the message to the channel as if it had been sent. The API contract is final only once the backend PRs merge; see the open questions below.Ticket
API surface
Sending
SendMessageOptions.scheduled_at?: string | Date. When the response hasscheduled_message, the message is stored to be sent later.response.messageis the content as it will be sent; it is not in the channel, and itsidis the scheduled message ID. The response'sscheduled_messageis upserted intoclient.scheduledMessages.ChannelQueryOptions.include_scheduled_messages?: boolean(client-side only).ChannelAPIResponse.scheduled_messages?replaces that channel's entries in the store. If the field is absent (the server did not know the list, e.g. more than 100), the store is left as it is.Client methods
updateScheduledMessage(id, { message?, scheduled_at? })PATCH /scheduled_messages/{id}deleteScheduledMessage(id)(cancels a scheduled message or removes a failed one)DELETE /scheduled_messages/{id}sendScheduledMessageNow(id)POST /scheduled_messages/{id}/send_nowqueryScheduledMessages({ filter: { cid?, status? }, sort?, limit?, next?, user_id? })POST /scheduled_messages/queryEach returns
{ scheduled_message }, except the query, which returns{ scheduled_messages, next? }.Store:
client.scheduledMessages(ScheduledMessageManager)state: StateStore<{ scheduledMessages: Map<id, ScheduledMessageResponse>, hasMore, isLoading, isLoaded, lastQueryError?, next? }>registerSubscriptions()/unregisterSubscriptions(), the same opt-in asReminderManagerload()(limit 100, replaces the store) andloadMore()getForChannel(cid, { parentId? }), sorted byscheduled_at.parentId: '<id>'returns that thread's replies,parentId: nullonly messages outside threads, and leaving it out returns bothupdateScheduledMessage,deleteScheduledMessageandsendScheduledMessageNow, which call the API and then update the storedisconnectUser(). IDs of sent and deleted messages are remembered, so a late response or event (e.g. the send-now response arriving afterscheduled_message.sent) cannot bring a message back.updated_at) is ignored, so the send-now response or its.updatedevent cannot turn a message that already failed back intoscheduled: the backend queues the send before it answers.load()and channel query hydration keep changes that arrived while they were in flight. Responses to requests made beforedisconnectUser()are dropped, so the next user never sees the previous user's messages.Types and events
ScheduledMessageResponse,ScheduledMessageContent,ScheduledMessageStatus(scheduled | sending | failed | canceled | sent),ScheduledMessageFailureReason,ScheduledMessageChannelResponse,ScheduledMessageLocation, and the request and response types.scheduled_message.created,.updated,.deleted,.sent(with a top-levelmessage_id, the final message ID) and.failed.Event.scheduled_messageis added.ScheduledMessagesDisabledError...ScheduledMessageConflictError) are added toAPIErrorCodes. Only the conflict error (122) is marked retryable.APIErrorResponse.exception_fieldsis typed, because the backend puts the error name inexception_fields.reason.Backend contract notes (CHA-5924)
Resolved by merged backend PRs:
status: "sending"(GetStream/chat#18730). The query's status filter matches the reported status, and 409scheduled_message_sendinghappens exactly when the status issending.pending: falsecan now be sent together withscheduled_at(#18727); onlypending: trueis rejected.message.mentioned_usersnow holds user objects (#18731).scheduled_message.message.mentioned_usersstill holds user IDs.include_scheduled_messagesis client-side only (confirmed).scheduled_message.messagein v1, the same as on regular messages.Still worth knowing:
message.idis the scheduled message ID while the message is not in the channel (by design). The React SDK must drop its optimistic message whenscheduled_messageis present.hasMoreandloadMore()for this.Target branch
This PR targets v9 (
master). It will need porting torelease-v10for React SDK v15.Testing
test/unit/scheduledMessages/: the store (upsert, remove, terminal statuses, overtaken updates, channel hydration, disconnect reset, all five events, reload on recovery, load and loadMore, errors) and the API (URLs and payloads, sort normalization,scheduled_atpassthrough, offline queue bypass,include_scheduled_messageshydration)..updatedevent after.failed, a message scheduled or failed whileload()or a channel query is in flight, and load, send-now, sendMessage and channel query responses arriving afterdisconnectUser(). Without the ordering fix, 10 of them fail.yarn types,yarn lint,yarn buildand the full unit suite (3252 passed; the scheduled message suites have 45 tests) all pass. The suite also reports one unhandled rejection fromtest/unit/poll.test.js, which comes from master and not from this change.CLA
Changelog
None written by hand: semantic-release builds the changelog from the
feat:commit.🤖 Generated with Claude Code