[real-time] Promisify RealTimeClient (#30500)

* Define `Client` at the end of the file

* Promisify `setSession`

* Promisify `setAnonSession`

* Remove unused `unsetSession`

* Promisify `connect`

* Promisify `getConnectedClients`, add `@overleaf/fetch-utils`

* Promisify `countConnectedClients`

* Promisify `getConnectedClient`

* Promisify `disconnectClient`

* Promisify `disconnectAllClients`

* Remove decaffeinate comment

GitOrigin-RevId: 4b0b376cdae5b1b69fe7e66b131d4b2ec7059eaa
This commit is contained in:
Antoine Clausse
2026-01-08 16:30:19 +01:00
committed by Copybot
parent 861b66b007
commit 7dc937b0f0
3 changed files with 113 additions and 133 deletions

View File

@@ -39,6 +39,7 @@
"zod-validation-error": "^4.0.1"
},
"devDependencies": {
"@overleaf/fetch-utils": "*",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"cookie-signature": "^1.1.0",

View File

@@ -1,26 +1,14 @@
/* eslint-disable
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let Client
const { XMLHttpRequest } = require('../../libs/XMLHttpRequest')
const io = require('socket.io-client')
const async = require('async')
const request = require('request')
const Settings = require('@overleaf/settings')
const redis = require('@overleaf/redis-wrapper')
const rclient = redis.createClient(Settings.redis.websessions)
const uid = require('uid-safe').sync
const signature = require('cookie-signature')
const { callbackify } = require('node:util')
const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
io.util.request = function () {
const xhr = new XMLHttpRequest()
@@ -34,142 +22,132 @@ io.util.request = function () {
return xhr
}
module.exports = Client = {
cookie: null,
async function setSession(session) {
const sessionId = uid(24)
session.cookie = {}
setSession(session, callback) {
if (callback == null) {
callback = function () {}
}
const sessionId = uid(24)
session.cookie = {}
return rclient.set('sess:' + sessionId, JSON.stringify(session), error => {
if (error != null) {
return callback(error)
}
Client.cookieSignedWith = {}
// prepare cookie strings for all supported session secrets
for (const secretName of [
'sessionSecret',
'sessionSecretFallback',
'sessionSecretUpcoming',
]) {
const secret = Settings.security[secretName]
const cookieKey = 's:' + signature.sign(sessionId, secret)
Client.cookieSignedWith[secretName] =
`${Settings.cookieName}=${cookieKey}`
}
// default to the current session secret
Client.cookie = Client.cookieSignedWith.sessionSecret
return callback()
})
},
await rclient.set('sess:' + sessionId, JSON.stringify(session))
setAnonSession(projectId, anonymousAccessToken, callback) {
Client.setSession(
{
anonTokenAccess: {
[projectId]: anonymousAccessToken,
},
},
callback
)
},
Client.cookieSignedWith = {}
// prepare cookie strings for all supported session secrets
for (const secretName of [
'sessionSecret',
'sessionSecretFallback',
'sessionSecretUpcoming',
]) {
const secret = Settings.security[secretName]
const cookieKey = 's:' + signature.sign(sessionId, secret)
Client.cookieSignedWith[secretName] = `${Settings.cookieName}=${cookieKey}`
}
// default to the current session secret
Client.cookie = Client.cookieSignedWith.sessionSecret
}
unsetSession(callback) {
if (callback == null) {
callback = function () {}
}
Client.cookie = null
return callback()
},
async function setAnonSession(projectId, anonymousAccessToken) {
await Client.promises.setSession({
anonTokenAccess: {
[projectId]: anonymousAccessToken,
},
})
}
connect(projectId, callback) {
const client = io.connect('http://127.0.0.1:3026', {
'force new connection': true,
query: new URLSearchParams({ projectId }).toString(),
})
let disconnected = false
client.on('disconnect', () => {
disconnected = true
})
function connect(projectId) {
const client = io.connect('http://127.0.0.1:3026', {
'force new connection': true,
query: new URLSearchParams({ projectId }).toString(),
})
let disconnected = false
client.on('disconnect', () => {
disconnected = true
})
const promise = new Promise((resolve, reject) => {
client.on('connectionRejected', err => {
// Wait for disconnect ahead of continuing with the test sequence.
setTimeout(() => {
if (!disconnected) {
throw new Error('should disconnect after connectionRejected')
}
callback(err)
reject(err)
}, 10)
})
client.on('joinProjectResponse', resp => {
const { publicId, project, permissionsLevel, protocolVersion } = resp
client.publicId = publicId
callback(null, project, permissionsLevel, protocolVersion)
resolve({ project, permissionsLevel, protocolVersion, client })
})
})
return { client, promise }
}
async function getConnectedClients() {
return await fetchJson('http://127.0.0.1:3026/clients')
}
async function countConnectedClients(projectId) {
return await fetchJson(
`http://127.0.0.1:3026/project/${projectId}/count-connected-clients`
)
}
async function getConnectedClient(clientId) {
try {
return await fetchJson(`http://127.0.0.1:3026/clients/${clientId}`)
} catch (err) {
if (err.info?.status === 404) throw new Error('not found')
throw err
}
}
async function disconnectClient(clientId) {
await fetchNothing(`http://127.0.0.1:3026/client/${clientId}/disconnect`, {
method: 'POST',
})
}
async function disconnectAllClients() {
const clients = await Client.promises.getConnectedClients()
await Promise.all(
clients.map(clientView =>
Client.promises.disconnectClient(clientView.client_id)
)
)
}
const Client = {
cookie: null,
setSession: callbackify(setSession),
setAnonSession: callbackify(setAnonSession),
connect: (projectId, callback) => {
const { client, promise } = connect(projectId)
if (callback) {
promise
.then(({ project, permissionsLevel, protocolVersion }) =>
callback(null, project, permissionsLevel, protocolVersion)
)
.catch(err => callback(err))
}
return client
},
getConnectedClients(callback) {
if (callback == null) {
callback = function () {}
}
return request.get(
{
url: 'http://127.0.0.1:3026/clients',
json: true,
},
(error, response, data) => callback(error, data)
)
},
countConnectedClients(projectId, callback) {
request.get(
{
url: `http://127.0.0.1:3026/project/${projectId}/count-connected-clients`,
json: true,
},
(error, response, data) => callback(error, data)
)
},
getConnectedClient(clientId, callback) {
if (callback == null) {
callback = function () {}
}
return request.get(
{
url: `http://127.0.0.1:3026/clients/${clientId}`,
json: true,
},
(error, response, data) => {
if (response?.statusCode === 404) {
callback(new Error('not found'))
} else {
callback(error, data)
}
}
)
},
disconnectClient(clientId, callback) {
request.post(
{
url: `http://127.0.0.1:3026/client/${clientId}/disconnect`,
},
(error, response, data) => callback(error, data)
)
return null
},
disconnectAllClients(callback) {
return Client.getConnectedClients((error, clients) => {
if (error) return callback(error)
async.each(
clients,
(clientView, cb) => Client.disconnectClient(clientView.client_id, cb),
callback
)
})
getConnectedClients: callbackify(getConnectedClients),
countConnectedClients: callbackify(countConnectedClients),
getConnectedClient: callbackify(getConnectedClient),
disconnectClient: callbackify(disconnectClient),
disconnectAllClients: callbackify(disconnectAllClients),
promises: {
setSession,
setAnonSession,
connect: async projectId => {
const { client, promise } = connect(projectId)
const { project, permissionsLevel, protocolVersion } = await promise
return { project, permissionsLevel, protocolVersion, client }
},
getConnectedClients,
countConnectedClients,
getConnectedClient,
disconnectClient,
disconnectAllClients,
},
}
module.exports = Client