perf: cache media and coalesce realtime sync

This commit is contained in:
winchen
2026-08-03 10:14:02 +08:00
parent b492a5ff76
commit e42bd6a225
9 changed files with 187 additions and 47 deletions
+11
View File
@@ -6,6 +6,10 @@ server {
root /var/www/im-web; root /var/www/im-web;
index index.html; index index.html;
client_max_body_size 100m; client_max_body_size 100m;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types application/json;
location = / { location = / {
try_files /index.html =404; try_files /index.html =404;
@@ -18,6 +22,13 @@ server {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
} }
location = /api/media_proxy.php {
include snippets/fastcgi-php.conf;
fastcgi_param PHP_ADMIN_VALUE "open_basedir=/var/www/im-web/:/tmp/";
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 300;
}
location ~ ^/api/.+\.php$ { location ~ ^/api/.+\.php$ {
include snippets/fastcgi-php.conf; include snippets/fastcgi-php.conf;
fastcgi_param PHP_ADMIN_VALUE "open_basedir=/var/www/im-web/:/tmp/"; fastcgi_param PHP_ADMIN_VALUE "open_basedir=/var/www/im-web/:/tmp/";
+51 -15
View File
@@ -1521,11 +1521,27 @@ function loadRecentConversations(PDO $db, string $userId, int $limit): array
return array_slice($conversations, 0, $limit); return array_slice($conversations, 0, $limit);
} }
function messageSyncResult(
PDO $db,
string $userId,
array $messages,
bool $includeConversations,
int $limit
): array {
$result = ['messages' => $messages];
if ($includeConversations) {
$result['conversations'] = loadRecentConversations($db, $userId, $limit);
}
return $result;
}
$payload = readJsonPayload(); $payload = readJsonPayload();
$userId = trim((string)($payload['userId'] ?? '')); $userId = trim((string)($payload['userId'] ?? ''));
$target = trim((string)($payload['target'] ?? '')); $target = trim((string)($payload['target'] ?? ''));
$limit = clampLimit($payload['limit'] ?? 50, 50, 100); $limit = clampLimit($payload['limit'] ?? 50, 50, 100);
$markRead = payloadBool($payload['markRead'] ?? false); $markRead = payloadBool($payload['markRead'] ?? false);
$includeConversations = payloadBool($payload['includeConversations'] ?? false);
$conversationType = normalizeConversationType($payload['conversationType'] ?? $payload['type'] ?? 0); $conversationType = normalizeConversationType($payload['conversationType'] ?? $payload['type'] ?? 0);
if ($userId === '') { if ($userId === '') {
@@ -1540,28 +1556,48 @@ try {
if ($target !== '') { if ($target !== '') {
if ($conversationType === 'group') { if ($conversationType === 'group') {
if (isWebGroupTarget(imAppDb(), $target)) { if (isWebGroupTarget(imAppDb(), $target)) {
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), $db,
]); $userId,
loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
$includeConversations,
$limit
));
} }
if (isNativeGroupTarget($db, $target)) { if (isNativeGroupTarget($db, $target)) {
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadNativeGroupMessages($db, $userId, $target, $limit, $markRead), $db,
]); $userId,
loadNativeGroupMessages($db, $userId, $target, $limit, $markRead),
$includeConversations,
$limit
));
} }
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => [], $db,
]); $userId,
[],
$includeConversations,
$limit
));
} }
if (isWebGroupTarget(imAppDb(), $target)) { if (isWebGroupTarget(imAppDb(), $target)) {
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), $db,
]); $userId,
loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
$includeConversations,
$limit
));
} }
$targetId = resolveUserId($db, $target); $targetId = resolveUserId($db, $target);
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadConversationMessages($db, $userId, $targetId, $limit, $markRead), $db,
]); $userId,
loadConversationMessages($db, $userId, $targetId, $limit, $markRead),
$includeConversations,
$limit
));
} }
respond(0, 'success', [ respond(0, 'success', [
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web IM</title><link rel="icon" href="img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico"><script src="config/runtime-config.js"></script><script defer="defer" src="js/chunk-vendors.86756863.js"></script><script defer="defer" src="js/app.a89973f7.js"></script><link href="css/app.2c52dabb.css" rel="stylesheet"></head><body><noscript>请启用 JavaScript 后使用 Web IM。</noscript><div id="app"></div></body></html> <!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web IM</title><link rel="icon" href="img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico"><script src="config/runtime-config.js"></script><script defer="defer" src="js/chunk-vendors.86756863.js"></script><script defer="defer" src="js/app.631b8b2d.js"></script><link href="css/app.2c52dabb.css" rel="stylesheet"></head><body><noscript>请启用 JavaScript 后使用 Web IM。</noscript><div id="app"></div></body></html>
+34 -8
View File
@@ -6281,7 +6281,8 @@ class HttpImClient {
target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''), target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''),
limit, limit,
conversationType: conversation.type || conversation.raw?.type || 0, conversationType: conversation.type || conversation.raw?.type || 0,
markRead: Boolean(options.markRead) markRead: Boolean(options.markRead),
includeConversations: Boolean(options.includeConversations)
}, { }, {
withCredentials: false withCredentials: false
}); });
@@ -6293,6 +6294,19 @@ class HttpImClient {
displayTimestamp: message.displayTimestamp || message.timestamp, displayTimestamp: message.displayTimestamp || message.timestamp,
conversationId: conversation.id conversationId: conversation.id
})); }));
if (options.includeConversations && Array.isArray(response.data?.result?.conversations)) {
const remoteConversations = response.data.result.conversations.map(remoteConversation => ({
...remoteConversation,
updatedAt: Number(remoteConversation.updatedAt || Date.now()),
avatarText: remoteConversation.avatarText || String(remoteConversation.title || remoteConversation.target || '?').slice(0, 1).toUpperCase(),
raw: remoteConversation.raw || {
type: 'single',
target: remoteConversation.target,
line: 0
}
}));
this.emit('conversationsChanged', remoteConversations);
}
const userId = this.currentUser?.userId || ''; const userId = this.currentUser?.userId || '';
const messagesByConversation = readLocalMessages(userId); const messagesByConversation = readLocalMessages(userId);
messagesByConversation[conversation.id] = mergeSyncedMessages(messagesByConversation[conversation.id] || [], messages, conversation.id, userId); messagesByConversation[conversation.id] = mergeSyncedMessages(messagesByConversation[conversation.id] || [], messages, conversation.id, userId);
@@ -8015,6 +8029,7 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
readPresenceEventsBound: false, readPresenceEventsBound: false,
syncInFlight: false, syncInFlight: false,
pendingReadReceiptConversationId: '', pendingReadReceiptConversationId: '',
pendingConversationSync: false,
conversationSyncInFlight: false, conversationSyncInFlight: false,
messageSyncTimer: null, messageSyncTimer: null,
messageSyncQueuedAt: 0, messageSyncQueuedAt: 0,
@@ -8126,6 +8141,7 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
this.syncInFlight = false; this.syncInFlight = false;
this.conversationSyncInFlight = false; this.conversationSyncInFlight = false;
this.pendingReadReceiptConversationId = ''; this.pendingReadReceiptConversationId = '';
this.pendingConversationSync = false;
this.lastReadSyncByConversation = {}; this.lastReadSyncByConversation = {};
this.activeId = ''; this.activeId = '';
this.conversations = []; this.conversations = [];
@@ -8560,6 +8576,9 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
if (wantsReadReceipt) { if (wantsReadReceipt) {
this.pendingReadReceiptConversationId = conversation.id; this.pendingReadReceiptConversationId = conversation.id;
} }
if (options.includeConversations) {
this.pendingConversationSync = true;
}
return; return;
} }
this.syncInFlight = true; this.syncInFlight = true;
@@ -8567,7 +8586,8 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
const previousMessages = this.messagesByConversation[syncedConversationId] || []; const previousMessages = this.messagesByConversation[syncedConversationId] || [];
try { try {
const messages = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__/* .imClient */ .wD.fetchRemoteMessages(conversation, 50, { const messages = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__/* .imClient */ .wD.fetchRemoteMessages(conversation, 50, {
markRead markRead,
includeConversations: Boolean(options.includeConversations)
}); });
if (markRead) { if (markRead) {
this.lastReadSyncByConversation = { this.lastReadSyncByConversation = {
@@ -8608,10 +8628,13 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
// Keep the local view usable if a polling request misses. // Keep the local view usable if a polling request misses.
} finally { } finally {
this.syncInFlight = false; this.syncInFlight = false;
if (this.pendingReadReceiptConversationId === this.activeId && canMarkReadNow()) { if ((this.pendingReadReceiptConversationId === this.activeId || this.pendingConversationSync) && canMarkReadNow()) {
const includeConversations = this.pendingConversationSync;
this.pendingReadReceiptConversationId = ''; this.pendingReadReceiptConversationId = '';
this.pendingConversationSync = false;
this.scheduleActiveMessagesSync({ this.scheduleActiveMessagesSync({
allowReadReceipt: true allowReadReceipt: true,
includeConversations
}, 700); }, 700);
} }
} }
@@ -8627,7 +8650,8 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
this.pendingMessageSyncOptions = { this.pendingMessageSyncOptions = {
...this.pendingMessageSyncOptions, ...this.pendingMessageSyncOptions,
allowReadReceipt: Boolean(this.pendingMessageSyncOptions.allowReadReceipt || options.allowReadReceipt), allowReadReceipt: Boolean(this.pendingMessageSyncOptions.allowReadReceipt || options.allowReadReceipt),
userAction: Boolean(this.pendingMessageSyncOptions.userAction || options.userAction) userAction: Boolean(this.pendingMessageSyncOptions.userAction || options.userAction),
includeConversations: Boolean(this.pendingMessageSyncOptions.includeConversations || options.includeConversations)
}; };
const maxWait = 1200; const maxWait = 1200;
const runAt = Math.min(now + Math.max(0, Number(delay) || 0), this.messageSyncQueuedAt + maxWait); const runAt = Math.min(now + Math.max(0, Number(delay) || 0), this.messageSyncQueuedAt + maxWait);
@@ -8664,11 +8688,13 @@ const useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__/* .defineSto
} }
window.clearTimeout(this.realtimeSyncTimer); window.clearTimeout(this.realtimeSyncTimer);
this.realtimeSyncTimer = window.setTimeout(() => { this.realtimeSyncTimer = window.setTimeout(() => {
this.refreshRemoteConversations();
if (this.activeId) { if (this.activeId) {
this.scheduleActiveMessagesSync({ this.scheduleActiveMessagesSync({
allowReadReceipt: canMarkReadNow() allowReadReceipt: canMarkReadNow(),
includeConversations: true
}, 700); }, 700);
} else {
this.refreshRemoteConversations();
} }
}, 500); }, 500);
}, },
@@ -9874,4 +9900,4 @@ const __exports__ = _NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBP
/******/ /******/
/******/ })() /******/ })()
; ;
//# sourceMappingURL=app.a89973f7.js.map //# sourceMappingURL=app.631b8b2d.js.map
+1
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
+51 -15
View File
@@ -1521,11 +1521,27 @@ function loadRecentConversations(PDO $db, string $userId, int $limit): array
return array_slice($conversations, 0, $limit); return array_slice($conversations, 0, $limit);
} }
function messageSyncResult(
PDO $db,
string $userId,
array $messages,
bool $includeConversations,
int $limit
): array {
$result = ['messages' => $messages];
if ($includeConversations) {
$result['conversations'] = loadRecentConversations($db, $userId, $limit);
}
return $result;
}
$payload = readJsonPayload(); $payload = readJsonPayload();
$userId = trim((string)($payload['userId'] ?? '')); $userId = trim((string)($payload['userId'] ?? ''));
$target = trim((string)($payload['target'] ?? '')); $target = trim((string)($payload['target'] ?? ''));
$limit = clampLimit($payload['limit'] ?? 50, 50, 100); $limit = clampLimit($payload['limit'] ?? 50, 50, 100);
$markRead = payloadBool($payload['markRead'] ?? false); $markRead = payloadBool($payload['markRead'] ?? false);
$includeConversations = payloadBool($payload['includeConversations'] ?? false);
$conversationType = normalizeConversationType($payload['conversationType'] ?? $payload['type'] ?? 0); $conversationType = normalizeConversationType($payload['conversationType'] ?? $payload['type'] ?? 0);
if ($userId === '') { if ($userId === '') {
@@ -1540,28 +1556,48 @@ try {
if ($target !== '') { if ($target !== '') {
if ($conversationType === 'group') { if ($conversationType === 'group') {
if (isWebGroupTarget(imAppDb(), $target)) { if (isWebGroupTarget(imAppDb(), $target)) {
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), $db,
]); $userId,
loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
$includeConversations,
$limit
));
} }
if (isNativeGroupTarget($db, $target)) { if (isNativeGroupTarget($db, $target)) {
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadNativeGroupMessages($db, $userId, $target, $limit, $markRead), $db,
]); $userId,
loadNativeGroupMessages($db, $userId, $target, $limit, $markRead),
$includeConversations,
$limit
));
} }
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => [], $db,
]); $userId,
[],
$includeConversations,
$limit
));
} }
if (isWebGroupTarget(imAppDb(), $target)) { if (isWebGroupTarget(imAppDb(), $target)) {
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), $db,
]); $userId,
loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
$includeConversations,
$limit
));
} }
$targetId = resolveUserId($db, $target); $targetId = resolveUserId($db, $target);
respond(0, 'success', [ respond(0, 'success', messageSyncResult(
'messages' => loadConversationMessages($db, $userId, $targetId, $limit, $markRead), $db,
]); $userId,
loadConversationMessages($db, $userId, $targetId, $limit, $markRead),
$includeConversations,
$limit
));
} }
respond(0, 'success', [ respond(0, 'success', [
+11 -1
View File
@@ -379,7 +379,8 @@ export class HttpImClient {
target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''), target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''),
limit, limit,
conversationType: conversation.type || conversation.raw?.type || 0, conversationType: conversation.type || conversation.raw?.type || 0,
markRead: Boolean(options.markRead) markRead: Boolean(options.markRead),
includeConversations: Boolean(options.includeConversations)
}, { withCredentials: false }); }, { withCredentials: false });
if (response.data?.code !== 0) { if (response.data?.code !== 0) {
@@ -391,6 +392,15 @@ export class HttpImClient {
displayTimestamp: message.displayTimestamp || message.timestamp, displayTimestamp: message.displayTimestamp || message.timestamp,
conversationId: conversation.id conversationId: conversation.id
})); }));
if (options.includeConversations && Array.isArray(response.data?.result?.conversations)) {
const remoteConversations = response.data.result.conversations.map(remoteConversation => ({
...remoteConversation,
updatedAt: Number(remoteConversation.updatedAt || Date.now()),
avatarText: remoteConversation.avatarText || String(remoteConversation.title || remoteConversation.target || '?').slice(0, 1).toUpperCase(),
raw: remoteConversation.raw || { type: 'single', target: remoteConversation.target, line: 0 }
}));
this.emit('conversationsChanged', remoteConversations);
}
const userId = this.currentUser?.userId || ''; const userId = this.currentUser?.userId || '';
const messagesByConversation = readLocalMessages(userId); const messagesByConversation = readLocalMessages(userId);
messagesByConversation[conversation.id] = mergeSyncedMessages(messagesByConversation[conversation.id] || [], messages, conversation.id, userId); messagesByConversation[conversation.id] = mergeSyncedMessages(messagesByConversation[conversation.id] || [], messages, conversation.id, userId);
+27 -6
View File
@@ -345,6 +345,7 @@ export const useConversationStore = defineStore('conversations', {
readPresenceEventsBound: false, readPresenceEventsBound: false,
syncInFlight: false, syncInFlight: false,
pendingReadReceiptConversationId: '', pendingReadReceiptConversationId: '',
pendingConversationSync: false,
conversationSyncInFlight: false, conversationSyncInFlight: false,
messageSyncTimer: null, messageSyncTimer: null,
messageSyncQueuedAt: 0, messageSyncQueuedAt: 0,
@@ -457,6 +458,7 @@ export const useConversationStore = defineStore('conversations', {
this.syncInFlight = false; this.syncInFlight = false;
this.conversationSyncInFlight = false; this.conversationSyncInFlight = false;
this.pendingReadReceiptConversationId = ''; this.pendingReadReceiptConversationId = '';
this.pendingConversationSync = false;
this.lastReadSyncByConversation = {}; this.lastReadSyncByConversation = {};
this.activeId = ''; this.activeId = '';
this.conversations = []; this.conversations = [];
@@ -887,6 +889,9 @@ export const useConversationStore = defineStore('conversations', {
if (wantsReadReceipt) { if (wantsReadReceipt) {
this.pendingReadReceiptConversationId = conversation.id; this.pendingReadReceiptConversationId = conversation.id;
} }
if (options.includeConversations) {
this.pendingConversationSync = true;
}
return; return;
} }
this.syncInFlight = true; this.syncInFlight = true;
@@ -894,7 +899,8 @@ export const useConversationStore = defineStore('conversations', {
const previousMessages = this.messagesByConversation[syncedConversationId] || []; const previousMessages = this.messagesByConversation[syncedConversationId] || [];
try { try {
const messages = await imClient.fetchRemoteMessages(conversation, 50, { const messages = await imClient.fetchRemoteMessages(conversation, 50, {
markRead markRead,
includeConversations: Boolean(options.includeConversations)
}); });
if (markRead) { if (markRead) {
this.lastReadSyncByConversation = { this.lastReadSyncByConversation = {
@@ -938,9 +944,17 @@ export const useConversationStore = defineStore('conversations', {
// Keep the local view usable if a polling request misses. // Keep the local view usable if a polling request misses.
} finally { } finally {
this.syncInFlight = false; this.syncInFlight = false;
if (this.pendingReadReceiptConversationId === this.activeId && canMarkReadNow()) { if (
(this.pendingReadReceiptConversationId === this.activeId || this.pendingConversationSync)
&& canMarkReadNow()
) {
const includeConversations = this.pendingConversationSync;
this.pendingReadReceiptConversationId = ''; this.pendingReadReceiptConversationId = '';
this.scheduleActiveMessagesSync({ allowReadReceipt: true }, 700); this.pendingConversationSync = false;
this.scheduleActiveMessagesSync({
allowReadReceipt: true,
includeConversations
}, 700);
} }
} }
}, },
@@ -957,7 +971,10 @@ export const useConversationStore = defineStore('conversations', {
allowReadReceipt: Boolean( allowReadReceipt: Boolean(
this.pendingMessageSyncOptions.allowReadReceipt || options.allowReadReceipt this.pendingMessageSyncOptions.allowReadReceipt || options.allowReadReceipt
), ),
userAction: Boolean(this.pendingMessageSyncOptions.userAction || options.userAction) userAction: Boolean(this.pendingMessageSyncOptions.userAction || options.userAction),
includeConversations: Boolean(
this.pendingMessageSyncOptions.includeConversations || options.includeConversations
)
}; };
const maxWait = 1200; const maxWait = 1200;
const runAt = Math.min(now + Math.max(0, Number(delay) || 0), this.messageSyncQueuedAt + maxWait); const runAt = Math.min(now + Math.max(0, Number(delay) || 0), this.messageSyncQueuedAt + maxWait);
@@ -994,9 +1011,13 @@ export const useConversationStore = defineStore('conversations', {
} }
window.clearTimeout(this.realtimeSyncTimer); window.clearTimeout(this.realtimeSyncTimer);
this.realtimeSyncTimer = window.setTimeout(() => { this.realtimeSyncTimer = window.setTimeout(() => {
this.refreshRemoteConversations();
if (this.activeId) { if (this.activeId) {
this.scheduleActiveMessagesSync({ allowReadReceipt: canMarkReadNow() }, 700); this.scheduleActiveMessagesSync({
allowReadReceipt: canMarkReadNow(),
includeConversations: true
}, 700);
} else {
this.refreshRemoteConversations();
} }
}, 500); }, 500);
}, },