Fix chat/room ordering that could differ between devices (#45)

Two independent gaps, both fixed since the report was ambiguous about
which "chats" meant:

- ChatPane.tsx concatenated history (REST-fetched) and live (WS-pushed)
  without sorting, so anything that could desync receipt order from
  send order -- a rejoin/resync racing a still-in-flight WS message,
  which opening the same room on another device triggers directly via
  a fresh socket connection -- could render messages out of
  chronological order. Now sorted by created_at (stable sort, so
  same-timestamp messages keep their relative order).
- list_member_rooms/list_open_rooms/list_recent_messages ordered by
  created_at alone, with no secondary tiebreaker. Postgres doesn't
  guarantee a stable order for tied rows across separate query
  executions, so two rooms/messages sharing an identical timestamp
  (a real possibility -- rapid sends, bulk-created rooms) could come
  back in a different order on two separate fetches, i.e. two devices.
  Added id as a secondary sort key everywhere this showed up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:52:29 -06:00
co-authored by Claude Sonnet 5
parent c99a07cae1
commit a0e1565097
5 changed files with 100 additions and 5 deletions
+18 -2
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { NetworkError } from '../api/client'
import { getRoomMessages, markRoomRead } from '../api/rooms'
import type { ChatSocketHandle } from '../ws/useChatSocket'
@@ -127,6 +127,22 @@ export function ChatPane({
[socket, room.id, refreshHistory, markRead],
)
// history and live are just concatenated, not merge-sorted -- live is
// strictly receipt order, which isn't always send order. A rejoin (a
// reconnect, or opening the same room on another device) refetches
// history but doesn't guarantee anything about the timing of whatever
// WS messages land in live afterward relative to it, so without this
// sort a message can render above one that was actually sent earlier.
// Stable sort (guaranteed since ES2019) keeps same-timestamp messages in
// their original relative order rather than shuffling them.
const messages = useMemo(
() =>
[...history, ...live].sort(
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
),
[history, live],
)
const connected = socket.connected
const send = useCallback(
(content: string, imageId?: string, fileId?: string) => socket.send(room.id, content, imageId, fileId),
@@ -179,7 +195,7 @@ export function ChatPane({
<MessageList
roomId={room.id}
messages={[...history, ...live]}
messages={messages}
members={members}
onEdit={sendEdit}
onReact={sendReaction}