[web] fetch user details for chat messages/ranges in a single batch (#26342)

GitOrigin-RevId: 5a4238e30388bb7f58b7a93dda9e43338ab94f6e
This commit is contained in:
Jakob Ackermann
2025-06-12 09:11:27 +02:00
committed by Copybot
parent 3862826589
commit 365af778b6
2 changed files with 48 additions and 59 deletions

View File

@@ -1,61 +1,46 @@
const async = require('async')
const UserInfoManager = require('../User/UserInfoManager')
const UserInfoController = require('../User/UserInfoController')
const { promisify } = require('@overleaf/promise-utils')
const UserGetter = require('../User/UserGetter')
const { callbackify } = require('@overleaf/promise-utils')
function injectUserInfoIntoThreads(threads, callback) {
// There will be a lot of repitition of user_ids, so first build a list
// of unique ones to perform db look ups on, then use these to populate the
// user fields
let message, thread, threadId, userId
if (callback == null) {
callback = function () {}
}
const userIds = {}
for (threadId in threads) {
thread = threads[threadId]
async function injectUserInfoIntoThreads(threads) {
const userIds = new Set()
for (const thread of Object.values(threads)) {
if (thread.resolved) {
userIds[thread.resolved_by_user_id] = true
userIds.add(thread.resolved_by_user_id)
}
for (message of Array.from(thread.messages)) {
userIds[message.user_id] = true
for (const message of thread.messages) {
userIds.add(message.user_id)
}
}
const jobs = []
const users = {}
for (userId in userIds) {
;(userId =>
jobs.push(cb =>
UserInfoManager.getPersonalInfo(userId, function (error, user) {
if (error != null) return cb(error)
user = UserInfoController.formatPersonalInfo(user)
users[userId] = user
cb()
})
))(userId)
const projection = {
_id: true,
first_name: true,
last_name: true,
email: true,
}
return async.series(jobs, function (error) {
if (error != null) {
return callback(error)
const users = await UserGetter.promises.getUsers(userIds, projection)
const usersById = new Map()
for (const user of users) {
usersById.set(
user._id.toString(),
UserInfoController.formatPersonalInfo(user)
)
}
for (const thread of Object.values(threads)) {
if (thread.resolved) {
thread.resolved_by_user = usersById.get(thread.resolved_by_user_id)
}
for (threadId in threads) {
thread = threads[threadId]
if (thread.resolved) {
thread.resolved_by_user = users[thread.resolved_by_user_id]
}
for (message of Array.from(thread.messages)) {
message.user = users[message.user_id]
}
for (const message of thread.messages) {
message.user = usersById.get(message.user_id)
}
return callback(null, threads)
})
}
return threads
}
module.exports = {
injectUserInfoIntoThreads,
injectUserInfoIntoThreads: callbackify(injectUserInfoIntoThreads),
promises: {
injectUserInfoIntoThreads: promisify(injectUserInfoIntoThreads),
injectUserInfoIntoThreads,
},
}

View File

@@ -12,7 +12,7 @@ describe('ChatManager', function () {
this.user_id = 'mock-user-id'
this.ChatManager = SandboxedModule.require(modulePath, {
requires: {
'../User/UserInfoManager': (this.UserInfoManager = {}),
'../User/UserGetter': (this.UserGetter = { promises: {} }),
'../User/UserInfoController': (this.UserInfoController = {}),
},
})
@@ -32,18 +32,22 @@ describe('ChatManager', function () {
beforeEach(function () {
this.users = {
user_id_1: {
mock: 'user_1',
_id: 'user_id_1',
},
user_id_2: {
mock: 'user_2',
_id: 'user_id_2',
},
}
this.UserInfoManager.getPersonalInfo = (userId, callback) => {
return callback(null, this.users[userId])
}
sinon.spy(this.UserInfoManager, 'getPersonalInfo')
this.UserGetter.promises.getUsers = userIds =>
Promise.resolve(
Array.from(userIds)
.map(id => this.users[id])
.filter(u => !!u)
)
sinon.spy(this.UserGetter.promises, 'getUsers')
return (this.UserInfoController.formatPersonalInfo = user => ({
formatted: user.mock,
formatted: { id: user._id.toString() },
}))
})
@@ -79,16 +83,16 @@ describe('ChatManager', function () {
thread1: {
resolved: true,
resolved_by_user_id: 'user_id_1',
resolved_by_user: { formatted: 'user_1' },
resolved_by_user: { formatted: { id: 'user_id_1' } },
messages: [
{
user_id: 'user_id_1',
user: { formatted: 'user_1' },
user: { formatted: { id: 'user_id_1' } },
content: 'foo',
},
{
user_id: 'user_id_2',
user: { formatted: 'user_2' },
user: { formatted: { id: 'user_id_2' } },
content: 'bar',
},
],
@@ -97,7 +101,7 @@ describe('ChatManager', function () {
messages: [
{
user_id: 'user_id_1',
user: { formatted: 'user_1' },
user: { formatted: { id: 'user_id_1' } },
content: 'baz',
},
],
@@ -105,7 +109,7 @@ describe('ChatManager', function () {
})
})
it('should only need to look up each user once', async function () {
it('should lookup all users in a single batch', async function () {
await this.ChatManager.promises.injectUserInfoIntoThreads([
{
messages: [
@@ -121,7 +125,7 @@ describe('ChatManager', function () {
},
])
this.UserInfoManager.getPersonalInfo.calledOnce.should.equal(true)
this.UserGetter.promises.getUsers.should.have.been.calledOnce
})
})
})