fix(aim): count only incoming unread IMs with an index range scan
- commit
- d17e56d97fb0162770ab5be460bfd4de7e004e19
- author
- Kit · · 2026-09-29 06:01 UTC
- parent
- a826f44
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
1 file · +13 -10
M src/convex/aim.ts +13 -10
| @@ -121,19 +121,22 @@ export const getMyDirectRooms = authQuery({ | |||
| 121 | 121 | const otherId = (room.memberIds ?? []).find((id) => id !== me); | |
| 122 | 122 | const other = otherId ? await ctx.db.get(otherId) : null; | |
| 123 | 123 | const lastRead = readMap.get(room._id) ?? 0; | |
| 124 | - | // Walk newest-first until the read cursor, scanning at most MAX_UNREAD_COUNT | |
| 125 | - | // messages; a full scan still unread saturates the badge ("99+"). | |
| 126 | - | const recent = await ctx.db | |
| 124 | + | const lastMessage = await ctx.db | |
| 127 | 125 | .query('messages') | |
| 128 | 126 | .withIndex('by_chatRoom', (q) => q.eq('chatRoomId', room._id)) | |
| 129 | 127 | .order('desc') | |
| 130 | - | .take(MAX_UNREAD_COUNT); | |
| 131 | - | const lastMessage = recent[0] ?? null; | |
| 132 | - | const unread = recent.filter((m) => m.timestamp > lastRead); | |
| 133 | - | const unreadCount = | |
| 134 | - | unread.length === MAX_UNREAD_COUNT | |
| 135 | - | ? MAX_UNREAD_COUNT | |
| 136 | - | : unread.filter((m) => m.senderId !== me).length; | |
| 128 | + | .first(); | |
| 129 | + | // Incoming messages after the read cursor, capped for the "99+" badge. Every index | |
| 130 | + | // ends with _creationTime, so the range scan stops at the cursor. | |
| 131 | + | const unreadCount = ( | |
| 132 | + | await ctx.db | |
| 133 | + | .query('messages') | |
| 134 | + | .withIndex('by_chatRoom', (q) => | |
| 135 | + | q.eq('chatRoomId', room._id).gt('_creationTime', lastRead) | |
| 136 | + | ) | |
| 137 | + | .filter((q) => q.neq(q.field('senderId'), me)) | |
| 138 | + | .take(MAX_UNREAD_COUNT) | |
| 139 | + | ).length; | |
| 137 | 140 | return { | |
| 138 | 141 | id: room._id, | |
| 139 | 142 | other: other ? toPublicUser(other) : null, | |
| @@ -121,19 +121,22 @@ export const getMyDirectRooms = authQuery({ | |||||
| 121 | const otherId = (room.memberIds ?? []).find((id) => id !== me); | 121 | const otherId = (room.memberIds ?? []).find((id) => id !== me); | ||
| 122 | const other = otherId ? await ctx.db.get(otherId) : null; | 122 | const other = otherId ? await ctx.db.get(otherId) : null; | ||
| 123 | const lastRead = readMap.get(room._id) ?? 0; | 123 | const lastRead = readMap.get(room._id) ?? 0; | ||
| 124 | - | // Walk newest-first until the read cursor, scanning at most MAX_UNREAD_COUNT | 124 | + | const lastMessage = await ctx.db |
| 125 | - | // messages; a full scan still unread saturates the badge ("99+"). | |||
| 126 | - | const recent = await ctx.db | |||
| 127 | .query('messages') | 125 | .query('messages') | ||
| 128 | .withIndex('by_chatRoom', (q) => q.eq('chatRoomId', room._id)) | 126 | .withIndex('by_chatRoom', (q) => q.eq('chatRoomId', room._id)) | ||
| 129 | .order('desc') | 127 | .order('desc') | ||
| 130 | - | .take(MAX_UNREAD_COUNT); | 128 | + | .first(); |
| 131 | - | const lastMessage = recent[0] ?? null; | 129 | + | // Incoming messages after the read cursor, capped for the "99+" badge. Every index |
| 132 | - | const unread = recent.filter((m) => m.timestamp > lastRead); | 130 | + | // ends with _creationTime, so the range scan stops at the cursor. |
| 133 | - | const unreadCount = | 131 | + | const unreadCount = ( |
| 134 | - | unread.length === MAX_UNREAD_COUNT | 132 | + | await ctx.db |
| 135 | - | ? MAX_UNREAD_COUNT | 133 | + | .query('messages') |
| 136 | - | : unread.filter((m) => m.senderId !== me).length; | 134 | + | .withIndex('by_chatRoom', (q) => |
| 135 | + | q.eq('chatRoomId', room._id).gt('_creationTime', lastRead) | |||
| 136 | + | ) | |||
| 137 | + | .filter((q) => q.neq(q.field('senderId'), me)) | |||
| 138 | + | .take(MAX_UNREAD_COUNT) | |||
| 139 | + | ).length; | |||
| 137 | return { | 140 | return { | ||
| 138 | id: room._id, | 141 | id: room._id, | ||
| 139 | other: other ? toPublicUser(other) : null, | 142 | other: other ? toPublicUser(other) : null, | ||