From ebd3a0488da6fb412643dd42c781bb75d02e5be7 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 18 Dec 2025 16:33:04 -0500 Subject: [PATCH 1/8] WIP --- shared/constants/chat2/convostate.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index d6cb85cb070..3a0cac0ae99 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -551,7 +551,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const old = s.messageMap.get(mapOrdinal) if (old && old.type !== 'placeholder') { // ignore it - return + continue } } @@ -1033,6 +1033,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const meta = get().meta const tlfName = meta.tlfname const clientPrev = getClientPrev() + const convID = get().getConvID() // disable sending exploding messages if flag is false const ephemeralLifetime = get().explodingMode @@ -1059,7 +1060,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { ...ephemeralData, body: text, clientPrev, - conversationID: get().getConvID(), + conversationID: convID, identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, outboxID: undefined, replyTo, @@ -1416,6 +1417,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const f = async () => { const {id: conversationIDKey} = get() + const convID = get().getConvID() try { const res = await T.RPCChat.localLoadGalleryRpcListener({ incomingCallMap: { @@ -1453,7 +1455,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { }, }, params: { - convID: get().getConvID(), + convID, fromMsgID, num: 50, typ: viewType, @@ -1558,6 +1560,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { ) const loadingKey = Strings.waitingKeyChatThreadLoad(conversationIDKey) + const convID = get().getConvID() const onGotThread = (thread: string, why: string) => { if (!thread) { return @@ -1635,7 +1638,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { }, params: { cbMode: T.RPCChat.GetThreadNonblockCbMode.incremental, - conversationID: get().getConvID(), + conversationID: convID, identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, knownRemotes, pagination, From 527f7348a113d2c7f780962a000760010b80f8be Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 18 Dec 2025 16:36:31 -0500 Subject: [PATCH 2/8] WIP --- shared/constants/chat2/convostate.tsx | 31 ++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index 3a0cac0ae99..0ccece14c79 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -368,6 +368,8 @@ export const numMessagesOnInitialLoad = isMobile ? 20 : 100 export const numMessagesOnScrollback = isMobile ? 100 : 100 const createSlice: Z.ImmerStateCreator = (set, get) => { + let isLoadingMessages = false + const closeBotModal = () => { storeRegistry.getState('router').dispatch.clearModals() if (get().meta.teamname) { @@ -1516,9 +1518,14 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { forceClear = true } + if (isLoadingMessages && !forceClear) { + return + } + // clear immediately to avoid races and avoid desktop having to churn while it loads a lot of waypoints if (forceClear) { get().dispatch.messagesClear() + isLoadingMessages = false } const scrollDirectionToPagination = (sd: ScrollDirection, numberOfMessagesToLoad: number) => { @@ -1543,18 +1550,20 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { // we get a thread-is-stale notification, or when you scroll up and want more // messages const f = async () => { - // Get the conversationIDKey - const {id: conversationIDKey} = get() + isLoadingMessages = true + try { + // Get the conversationIDKey + const {id: conversationIDKey} = get() - if (!conversationIDKey || !T.Chat.isValidConversationIDKey(conversationIDKey)) { - logger.info('loadMoreMessages: bail: no conversationIDKey') - return - } + if (!conversationIDKey || !T.Chat.isValidConversationIDKey(conversationIDKey)) { + logger.info('loadMoreMessages: bail: no conversationIDKey') + return + } - if (get().meta.membershipType === 'youAreReset' || get().meta.rekeyers.size > 0) { - logger.info('loadMoreMessages: bail: we are reset') - return - } + if (get().meta.membershipType === 'youAreReset' || get().meta.rekeyers.size > 0) { + logger.info('loadMoreMessages: bail: we are reset') + return + } logger.info( `loadMoreMessages: calling rpc convo: ${conversationIDKey} num: ${numberOfMessagesToLoad} reason: ${reason}` ) @@ -1674,6 +1683,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { throw error } } + } finally { + isLoadingMessages = false } } From da7363303250cae5bf051bf2041fb8a358510b3d Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 18 Dec 2025 18:11:27 -0500 Subject: [PATCH 3/8] WIP --- shared/constants/chat2/convostate.tsx | 112 +++++++++++++------------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index 0ccece14c79..9a7f914c943 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -1564,13 +1564,13 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { logger.info('loadMoreMessages: bail: we are reset') return } - logger.info( - `loadMoreMessages: calling rpc convo: ${conversationIDKey} num: ${numberOfMessagesToLoad} reason: ${reason}` - ) + logger.info( + `loadMoreMessages: calling rpc convo: ${conversationIDKey} num: ${numberOfMessagesToLoad} reason: ${reason}` + ) - const loadingKey = Strings.waitingKeyChatThreadLoad(conversationIDKey) - const convID = get().getConvID() - const onGotThread = (thread: string, why: string) => { + const loadingKey = Strings.waitingKeyChatThreadLoad(conversationIDKey) + const convID = get().getConvID() + const onGotThread = (thread: string, why: string) => { if (!thread) { return } @@ -1630,57 +1630,58 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { } } - const pagination = messageIDControl ? null : scrollDirectionToPagination(sd, numberOfMessagesToLoad) - try { - const results = await T.RPCChat.localGetThreadNonblockRpcListener({ - incomingCallMap: { - 'chat.1.chatUi.chatThreadCached': p => onGotThread(p.thread || '', 'cached'), - 'chat.1.chatUi.chatThreadFull': p => onGotThread(p.thread || '', 'full'), - 'chat.1.chatUi.chatThreadStatus': p => { - logger.info( - `loadMoreMessages: thread status received: convID: ${conversationIDKey} typ: ${p.status.typ}` - ) - set(s => { - s.threadLoadStatus = p.status.typ - }) + const pagination = messageIDControl ? null : scrollDirectionToPagination(sd, numberOfMessagesToLoad) + try { + const results = await T.RPCChat.localGetThreadNonblockRpcListener({ + incomingCallMap: { + 'chat.1.chatUi.chatThreadCached': p => onGotThread(p.thread || '', 'cached'), + 'chat.1.chatUi.chatThreadFull': p => onGotThread(p.thread || '', 'full'), + 'chat.1.chatUi.chatThreadStatus': p => { + logger.info( + `loadMoreMessages: thread status received: convID: ${conversationIDKey} typ: ${p.status.typ}` + ) + set(s => { + s.threadLoadStatus = p.status.typ + }) + }, }, - }, - params: { - cbMode: T.RPCChat.GetThreadNonblockCbMode.incremental, - conversationID: convID, - identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, - knownRemotes, - pagination, - pgmode: T.RPCChat.GetThreadNonblockPgMode.server, - query: { - disablePostProcessThread: false, - disableResolveSupersedes: false, - enableDeletePlaceholders: true, - markAsRead: false, - messageIDControl, - messageTypes: loadThreadMessageTypes, + params: { + cbMode: T.RPCChat.GetThreadNonblockCbMode.incremental, + conversationID: convID, + identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, + knownRemotes, + pagination, + pgmode: T.RPCChat.GetThreadNonblockPgMode.server, + query: { + disablePostProcessThread: false, + disableResolveSupersedes: false, + enableDeletePlaceholders: true, + markAsRead: false, + messageIDControl, + messageTypes: loadThreadMessageTypes, + }, + reason: reasonToRPCReason(reason), }, - reason: reasonToRPCReason(reason), - }, - waitingKey: loadingKey, - }) - if (get().isMetaGood()) { - set(s => { - s.meta.offline = results.offline + waitingKey: loadingKey, }) - } - } catch (error) { - if (error instanceof RPCError) { - logger.warn(`loadMoreMessages: error: ${error.desc}`) - // no longer in team - if (error.code === T.RPCGen.StatusCode.scchatnotinteam) { - const {inboxRefresh, navigateToInbox} = storeRegistry.getState('chat').dispatch - inboxRefresh('maybeKickedFromTeam') - navigateToInbox() + if (get().isMetaGood()) { + set(s => { + s.meta.offline = results.offline + }) } - if (error.code !== T.RPCGen.StatusCode.scteamreaderror) { - // scteamreaderror = user is not in team. they'll see the rekey screen so don't throw for that - throw error + } catch (error) { + if (error instanceof RPCError) { + logger.warn(`loadMoreMessages: error: ${error.desc}`) + // no longer in team + if (error.code === T.RPCGen.StatusCode.scchatnotinteam) { + const {inboxRefresh, navigateToInbox} = storeRegistry.getState('chat').dispatch + inboxRefresh('maybeKickedFromTeam') + navigateToInbox() + } + if (error.code !== T.RPCGen.StatusCode.scteamreaderror) { + // scteamreaderror = user is not in team. they'll see the rekey screen so don't throw for that + throw error + } } } } finally { @@ -2915,7 +2916,10 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { if (message) { set(s => { - s.threadSearchInfo.hits.push(T.castDraft(message)) + // Only add if not already present (idempotent - safe for out-of-order callbacks) + if (!s.threadSearchInfo.hits.find(h => h.id === message.id)) { + s.threadSearchInfo.hits.push(T.castDraft(message)) + } }) } }, From c53a77a74c9a3852cde8570b37403419a582afc9 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 18 Dec 2025 19:44:20 -0500 Subject: [PATCH 4/8] WIP --- shared/constants/chat2/convostate.tsx | 146 ++++++++++++++++++++------ 1 file changed, 114 insertions(+), 32 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index 9a7f914c943..c0932e422de 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -116,9 +116,10 @@ type ConvoStore = T.Immutable<{ markedAsUnread: T.Chat.Ordinal maxMsgIDSeen: T.Chat.MessageID // max id weve seen so far, we do delete things messageCenterOrdinal?: T.Chat.CenterOrdinal // ordinals to center threads on, - messageTypeMap: Map // messages T.Chat to help the thread, text is never used - messageOrdinals?: ReadonlyArray // ordered ordinals in a thread, + messageIDToOrdinalMap: Map // reverse lookup for O(1) messageID -> ordinal messageMap: Map // messages in a thread, + messageOrdinals?: ReadonlyArray // ordered ordinals in a thread, + messageTypeMap: Map // messages T.Chat to help the thread, text is never used meta: T.Chat.ConversationMeta // metadata about a thread, There is a special node for the pending conversation, moreToLoadBack: boolean moreToLoadForward: boolean @@ -155,6 +156,7 @@ const initialConvoStore: ConvoStore = { markedAsUnread: T.Chat.numberToOrdinal(0), maxMsgIDSeen: T.Chat.numberToMessageID(-1), messageCenterOrdinal: undefined, + messageIDToOrdinalMap: new Map(), messageMap: new Map(), messageOrdinals: undefined, messageTypeMap: new Map(), @@ -340,8 +342,20 @@ const makeAttachmentViewInfo = (): T.Chat.AttachmentViewInfo => ({ const messageIDToOrdinal = ( map: ConvoState['messageMap'], pendingOutboxToOrdinal: ConvoState['pendingOutboxToOrdinal'] | undefined, - messageID: T.Chat.MessageID + messageID: T.Chat.MessageID, + messageIDToOrdinalMap?: ConvoState['messageIDToOrdinalMap'] ) => { + // Fast path: use reverse lookup map if available + if (messageIDToOrdinalMap) { + const cachedOrdinal = messageIDToOrdinalMap.get(messageID) + if (cachedOrdinal !== undefined) { + const m = map.get(cachedOrdinal) + if (m?.id !== 0 && m?.id === messageID) { + return cachedOrdinal + } + } + } + // A message we didn't send in this session? let m = map.get(T.Chat.numberToOrdinal(messageID)) if (m?.id !== 0 && m?.id === messageID) { @@ -449,6 +463,31 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { // things that depend on messageMap, like the ordinals and the maxMsgIDSeen const syncMessageDerived = (s: Z.WritableDraft) => { + const currentSize = s.messageOrdinals?.length ?? 0 + const mapSize = s.messageMap.size + + // Early exit: if sizes match and we have ordinals, check if we actually need to recalculate + if (currentSize === mapSize && s.messageOrdinals) { + // Quick check: verify all ordinals in messageOrdinals still exist and are regular messages + let needsRecalc = false + for (const ord of s.messageOrdinals) { + const m = s.messageMap.get(ord) + if (!m || m.conversationMessage === false) { + needsRecalc = true + break + } + } + if (!needsRecalc) { + // Still update maxMsgIDSeen in case it changed + const lastOrd = s.messageOrdinals.at(-1) + const lastID = lastOrd ? (s.messageMap.get(lastOrd)?.id ?? 0) : 0 + if (lastID && lastID > s.maxMsgIDSeen) { + s.maxMsgIDSeen = lastID + } + return + } + } + const mo = [...s.messageMap] .filter(([, m]) => { const regularMessage = m.conversationMessage !== false @@ -537,6 +576,10 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { s.maxMsgIDSeen = m.id } if (regularMessage && m.type === 'deleted') { + const oldMessage = s.messageMap.get(m.ordinal) + if (oldMessage && oldMessage.id !== 0) { + s.messageIDToOrdinalMap.delete(oldMessage.id) + } s.messageMap.delete(m.ordinal) s.messageTypeMap.delete(m.ordinal) } else { @@ -561,7 +604,15 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { m.ordinal = mapOrdinal } + const oldMessage = s.messageMap.get(mapOrdinal) + if (oldMessage && oldMessage.id !== 0 && oldMessage.id !== m.id) { + s.messageIDToOrdinalMap.delete(oldMessage.id) + } + s.messageMap.set(mapOrdinal, T.castDraft(m)) + if (regularMessage && m.id !== 0) { + s.messageIDToOrdinalMap.set(m.id, mapOrdinal) + } if ( regularMessage && m.outboxID && @@ -777,7 +828,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const ordinal = messageIDToOrdinal( get().messageMap, get().pendingOutboxToOrdinal, - T.Chat.numberToMessageID(msgID) + T.Chat.numberToMessageID(msgID), + get().messageIDToOrdinalMap ) if (!ordinal) { logger.info(`downloadComplete: no ordinal found: conversationIDKey: ${get().id} msgID: ${msgID}`) @@ -802,7 +854,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const ordinal = messageIDToOrdinal( get().messageMap, get().pendingOutboxToOrdinal, - T.Chat.numberToMessageID(msgID) + T.Chat.numberToMessageID(msgID), + get().messageIDToOrdinalMap ) if (!ordinal) { logger.info(`downloadProgress: no ordinal found: conversationIDKey: ${get().id} msgID: ${msgID}`) @@ -917,6 +970,9 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const existing = get().messageMap.get(toDelOrdinal) if (existing) { set(s => { + if (existing.id !== 0) { + s.messageIDToOrdinalMap.delete(existing.id) + } s.messageMap.delete(toDelOrdinal) syncMessageDerived(s) }) @@ -970,7 +1026,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const ordinal = messageIDToOrdinal( get().messageMap, get().pendingOutboxToOrdinal, - T.Chat.numberToMessageID(placeholderID) + T.Chat.numberToMessageID(placeholderID), + get().messageIDToOrdinalMap ) const existing = ordinal ? get().messageMap.get(ordinal) : undefined if (ordinal && existing) { @@ -1437,7 +1494,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { devicename ) - if (m) { + if (m) { // conversationMessage is used to tell if its this gallery load or not but if we // load a message we already have we don't want to overwrite that it really belongs const message = {...m, conversationMessage: get().messageMap.has(m.ordinal)} @@ -1448,7 +1505,20 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { T.castDraft(makeAttachmentViewInfo()) ) if (!info.messages.find(item => item.id === message.id)) { - info.messages = info.messages.concat(T.castDraft(message)).sort((l, r) => r.id - l.id) + const messages = info.messages + // Binary search to find insertion point for O(n) insertion instead of O(n log n) sort + let insertIndex = messages.length + for (let i = 0; i < messages.length; i++) { + if (messages[i].id < message.id) { + insertIndex = i + break + } + } + info.messages = [ + ...messages.slice(0, insertIndex), + T.castDraft(message), + ...messages.slice(insertIndex), + ] } }) // inject them into the message map @@ -2030,7 +2100,9 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { ignorePromise(f()) }, messagesClear: () => { + get().dispatch._clearConvIDCache() set(s => { + s.messageIDToOrdinalMap.clear() s.pendingOutboxToOrdinal.clear() s.loaded = false s.messageMap.clear() @@ -2043,7 +2115,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { logger.info(`messagesExploded: exploding ${messageIDs.length} messages`) set(s => { messageIDs.forEach(mid => { - const ordinal = messageIDToOrdinal(s.messageMap, s.pendingOutboxToOrdinal, mid) + const ordinal = messageIDToOrdinal(s.messageMap, s.pendingOutboxToOrdinal, mid, s.messageIDToOrdinalMap) const m = ordinal && s.messageMap.get(ordinal) if (!m) return m.exploded = true @@ -2065,33 +2137,36 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { ordinals = [], upToMessageID = null, } = p - const {pendingOutboxToOrdinal, messageMap} = get() + const {messageIDToOrdinalMap, pendingOutboxToOrdinal, messageMap} = get() - let upToOrdinals: Array = [] + const allOrdinals = new Set() + + // Add explicit ordinals + ordinals.forEach(ord => { + if (ord) allOrdinals.add(ord) + }) + + // Add ordinals from messageIDs using reverse lookup map (O(1) per lookup) + messageIDs.forEach(messageID => { + const ordinal = messageIDToOrdinalMap.get(messageID) ?? messageIDToOrdinal(messageMap, pendingOutboxToOrdinal, messageID, messageIDToOrdinalMap) + if (ordinal) allOrdinals.add(ordinal) + }) + + // Single pass: collect upToOrdinals and add to set in one iteration if (upToMessageID) { - upToOrdinals = [...messageMap.entries()].reduce((arr, [ordinal, m]) => { + for (const [ordinal, m] of messageMap.entries()) { if (m.id < upToMessageID && deletableMessageTypes.has(m.type)) { - arr.push(ordinal) - } - return arr - }, new Array()) - } - - const allOrdinals = new Set( - [ - ...ordinals, - ...messageIDs.map(messageID => messageIDToOrdinal(messageMap, pendingOutboxToOrdinal, messageID)), - ...upToOrdinals, - ].reduce>((arr, n) => { - if (n) { - arr.push(n) + allOrdinals.add(ordinal) } - return arr - }, []) - ) + } + } set(s => { allOrdinals.forEach(ordinal => { + const m = s.messageMap.get(ordinal) + if (m && m.id !== 0) { + s.messageIDToOrdinalMap.delete(m.id) + } s.messageMap.delete(ordinal) }) syncMessageDerived(s) @@ -2362,7 +2437,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { if (!message) { return } - const ordinal = messageIDToOrdinal(get().messageMap, get().pendingOutboxToOrdinal, messageID) + const ordinal = messageIDToOrdinal(get().messageMap, get().pendingOutboxToOrdinal, messageID, get().messageIDToOrdinalMap) set(s => { const existing = ordinal ? s.messageMap.get(ordinal) : undefined if (existing) { @@ -3181,7 +3256,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const targetOrdinal = messageIDToOrdinal( get().messageMap, get().pendingOutboxToOrdinal, - u.targetMsgID + u.targetMsgID, + get().messageIDToOrdinalMap ) if (!targetOrdinal) { logger.info( @@ -3202,9 +3278,15 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { }, } const convIDCache = new Map() + const clearConvIDCache = () => { + convIDCache.clear() + } return { ...initialConvoStore, - dispatch, + dispatch: { + ...dispatch, + _clearConvIDCache: clearConvIDCache, + }, getConvID: () => { const id = get().id if (!T.Chat.isValidConversationIDKey(id)) { From 7b4c8d173781632579706484d8410dcb00f70c71 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 19 Dec 2025 15:00:04 -0500 Subject: [PATCH 5/8] WIP --- shared/constants/chat2/convostate.tsx | 36 +++++++++++++++------------ 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index 603c2721679..36ec4e5efb5 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -475,12 +475,6 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { } } if (!needsRecalc) { - // Still update maxMsgIDSeen in case it changed - const lastOrd = s.messageOrdinals.at(-1) - const lastID = lastOrd ? (s.messageMap.get(lastOrd)?.id ?? 0) : 0 - if (lastID && lastID > s.maxMsgIDSeen) { - s.maxMsgIDSeen = lastID - } return } } @@ -1480,7 +1474,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { devicename ) - if (m) { + if (m) { // conversationMessage is used to tell if its this gallery load or not but if we // load a message we already have we don't want to overwrite that it really belongs const message = {...m, conversationMessage: get().messageMap.has(m.ordinal)} @@ -1495,7 +1489,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { // Binary search to find insertion point for O(n) insertion instead of O(n log n) sort let insertIndex = messages.length for (let i = 0; i < messages.length; i++) { - if (messages[i].id < message.id) { + const mi = messages[i] + if (mi && mi.id < message.id) { insertIndex = i break } @@ -2086,7 +2081,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { ignorePromise(f()) }, messagesClear: () => { - get().dispatch._clearConvIDCache() + clearConvIDCache() set(s => { s.messageIDToOrdinalMap.clear() s.pendingOutboxToOrdinal.clear() @@ -2100,7 +2095,12 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { logger.info(`messagesExploded: exploding ${messageIDs.length} messages`) set(s => { messageIDs.forEach(mid => { - const ordinal = messageIDToOrdinal(s.messageMap, s.pendingOutboxToOrdinal, mid, s.messageIDToOrdinalMap) + const ordinal = messageIDToOrdinal( + s.messageMap, + s.pendingOutboxToOrdinal, + mid, + s.messageIDToOrdinalMap + ) const m = ordinal && s.messageMap.get(ordinal) if (!m) return m.exploded = true @@ -2133,7 +2133,9 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { // Add ordinals from messageIDs using reverse lookup map (O(1) per lookup) messageIDs.forEach(messageID => { - const ordinal = messageIDToOrdinalMap.get(messageID) ?? messageIDToOrdinal(messageMap, pendingOutboxToOrdinal, messageID, messageIDToOrdinalMap) + const ordinal = + messageIDToOrdinalMap.get(messageID) ?? + messageIDToOrdinal(messageMap, pendingOutboxToOrdinal, messageID, messageIDToOrdinalMap) if (ordinal) allOrdinals.add(ordinal) }) @@ -2422,7 +2424,12 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { if (!message) { return } - const ordinal = messageIDToOrdinal(get().messageMap, get().pendingOutboxToOrdinal, messageID, get().messageIDToOrdinalMap) + const ordinal = messageIDToOrdinal( + get().messageMap, + get().pendingOutboxToOrdinal, + messageID, + get().messageIDToOrdinalMap + ) set(s => { const existing = ordinal ? s.messageMap.get(ordinal) : undefined if (existing) { @@ -3268,10 +3275,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { } return { ...initialConvoStore, - dispatch: { - ...dispatch, - _clearConvIDCache: clearConvIDCache, - }, + dispatch, getConvID: () => { const id = get().id if (!T.Chat.isValidConversationIDKey(id)) { From eefa59a076217c4669746ff76ef7ca4167e740ca Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Fri, 19 Dec 2025 15:30:38 -0500 Subject: [PATCH 6/8] WIP --- shared/constants/chat2/convostate.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index 36ec4e5efb5..c90e9045c0d 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -17,6 +17,7 @@ import * as Z from '@/util/zustand' import {makeActionForOpenPathInFilesTab} from '@/constants/fs/util' import HiddenString from '@/util/hidden-string' import isEqual from 'lodash/isEqual' +import sortedIndexBy from 'lodash/sortedIndexBy' import logger from '@/logger' import throttle from 'lodash/throttle' import type {DebouncedFunc} from 'lodash' @@ -1485,21 +1486,10 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { T.castDraft(makeAttachmentViewInfo()) ) if (!info.messages.find(item => item.id === message.id)) { - const messages = info.messages - // Binary search to find insertion point for O(n) insertion instead of O(n log n) sort - let insertIndex = messages.length - for (let i = 0; i < messages.length; i++) { - const mi = messages[i] - if (mi && mi.id < message.id) { - insertIndex = i - break - } - } - info.messages = [ - ...messages.slice(0, insertIndex), - T.castDraft(message), - ...messages.slice(insertIndex), - ] + // Use lodash sortedIndexBy with reversed comparator for descending order + // sortedIndexBy assumes ascending, so we negate the ID to reverse the sort + const insertIndex = sortedIndexBy(info.messages, message, m => -(m?.id ?? 0)) + info.messages.splice(insertIndex, 0, T.castDraft(message)) } }) // inject them into the message map From 7b0f1a1ebcd735cab32cf975b9afec10890f7da4 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 19 Dec 2025 15:34:38 -0500 Subject: [PATCH 7/8] WIP --- shared/constants/chat2/convostate.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index c90e9045c0d..3d597eb33a1 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -2071,7 +2071,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { ignorePromise(f()) }, messagesClear: () => { - clearConvIDCache() + convIDCache.clear() set(s => { s.messageIDToOrdinalMap.clear() s.pendingOutboxToOrdinal.clear() @@ -3260,9 +3260,6 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { }, } const convIDCache = new Map() - const clearConvIDCache = () => { - convIDCache.clear() - } return { ...initialConvoStore, dispatch, From fa8edc62c8e333d40b78ef9a3f9e5a6b63df03ac Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 19 Dec 2025 15:48:36 -0500 Subject: [PATCH 8/8] WIP --- shared/constants/chat2/convostate.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shared/constants/chat2/convostate.tsx b/shared/constants/chat2/convostate.tsx index 3d597eb33a1..b45a4688d05 100644 --- a/shared/constants/chat2/convostate.tsx +++ b/shared/constants/chat2/convostate.tsx @@ -1478,7 +1478,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { if (m) { // conversationMessage is used to tell if its this gallery load or not but if we // load a message we already have we don't want to overwrite that it really belongs - const message = {...m, conversationMessage: get().messageMap.has(m.ordinal)} + const message: T.Chat.Message = {...m, conversationMessage: get().messageMap.has(m.ordinal)} set(s => { const info = mapGetEnsureValue( s.attachmentViewMap, @@ -1488,7 +1488,11 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { if (!info.messages.find(item => item.id === message.id)) { // Use lodash sortedIndexBy with reversed comparator for descending order // sortedIndexBy assumes ascending, so we negate the ID to reverse the sort - const insertIndex = sortedIndexBy(info.messages, message, m => -(m?.id ?? 0)) + const insertIndex = sortedIndexBy( + info.messages, + message, + m => -T.Chat.messageIDToNumber(m.id) + ) info.messages.splice(insertIndex, 0, T.castDraft(message)) } })