Исправить восстановление DM после перелогина

This commit is contained in:
AidarKC
2026-07-10 11:47:39 +04:00
parent 0730837ad7
commit bb92cb6b2b
6 changed files with 130 additions and 20 deletions
@@ -51,6 +51,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
private static final SecureRandom RANDOM = new SecureRandom();
private static final long CLOSE_AFTER_ERROR_DELAY_MS = 75L;
private static final long SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS = 250L;
public static final long ALLOWED_SKEW_MS = 30_000L;
@@ -423,7 +424,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
ActiveConnectionsRegistry.getInstance().register(ctx);
SignedMessagesRealtime.dispatchPendingForSession(ctx);
SignedMessagesRealtime.dispatchPendingForSessionAsync(ctx, SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS);
// --- формируем ответ ---
Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
@@ -44,6 +44,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);
private static final long ALLOWED_SKEW_MS = 30_000L;
private static final long SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS = 250L;
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
@@ -301,7 +302,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
ActiveConnectionsRegistry.getInstance().register(ctx);
SignedMessagesRealtime.dispatchPendingForSession(ctx);
SignedMessagesRealtime.dispatchPendingForSessionAsync(ctx, SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS);
// ответ
Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
@@ -18,10 +18,22 @@ import java.util.Base64;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public final class SignedMessagesRealtime {
private static final Logger log = LoggerFactory.getLogger(SignedMessagesRealtime.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final ScheduledExecutorService BACKLOG_EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "signed-messages-backlog");
thread.setDaemon(true);
return thread;
}
});
private SignedMessagesRealtime() {}
@@ -63,7 +75,19 @@ public final class SignedMessagesRealtime {
String login = ctx.getLogin();
String sessionId = ctx.getSessionId();
if (isBlank(login) || isBlank(sessionId)) return;
dispatchPendingForSession(login, sessionId);
}
public static void dispatchPendingForSessionAsync(ConnectionContext ctx, long delayMs) {
if (ctx == null || !ctx.isAuthenticatedUser()) return;
String login = safeLower(ctx.getLogin());
String sessionId = String.valueOf(ctx.getSessionId() == null ? "" : ctx.getSessionId()).trim();
if (isBlank(login) || isBlank(sessionId)) return;
long safeDelayMs = Math.max(0L, delayMs);
BACKLOG_EXECUTOR.schedule(() -> dispatchPendingForSession(login, sessionId), safeDelayMs, TimeUnit.MILLISECONDS);
}
private static void dispatchPendingForSession(String login, String sessionId) {
try {
List<SignedMessageV2Entry> pending = SignedMessagesV2DAO.getInstance()
.listPendingForSession(login, sessionId);
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.2.301
server.version=1.2.280
client.version=1.2.302
server.version=1.2.281
+73 -16
View File
@@ -135,6 +135,8 @@ const toolbarEl = document.getElementById('toolbar-slot');
const appShellEl = document.querySelector('.app-shell');
const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000;
const SIGNED_DM_DECRYPT_CONTEXT_WAIT_MS = 2000;
const SIGNED_DM_DECRYPT_CONTEXT_POLL_MS = 50;
const UI_VERSION_PERIODIC_CHECK_MS = 5 * 60 * 1000;
const CURRENT_BUILD_HASH = String(window.__SHINE_BUILD_HASH__ || '').trim();
const UI_BUILD_HASH_PATTERN = /window\.__SHINE_BUILD_HASH__\s*=\s*'([^']+)'/;
@@ -641,6 +643,44 @@ function wsIsOpen() {
return !!(ws && ws.readyState === WebSocket.OPEN);
}
function sleep(ms) {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
async function waitForSignedDmDecryptContext(timeoutMs = SIGNED_DM_DECRYPT_CONTEXT_WAIT_MS) {
const startedAtMs = Date.now();
while ((Date.now() - startedAtMs) < timeoutMs) {
const login = String(state.session?.login || '').trim();
const storagePwd = String(state.session?.storagePwdInMemory || '').trim();
if (login && storagePwd) {
return { login, storagePwd };
}
await sleep(SIGNED_DM_DECRYPT_CONTEXT_POLL_MS);
}
return {
login: String(state.session?.login || '').trim(),
storagePwd: String(state.session?.storagePwdInMemory || '').trim(),
};
}
function resolveSignedDmChatId({ parsed, login }) {
const parsedBlock = parsed || {};
const messageType = Number(parsedBlock.messageType || 0);
const fromLogin = String(parsedBlock.fromLogin || '').trim();
const toLogin = String(parsedBlock.toLogin || '').trim();
const currentLogin = String(login || '').trim().toLowerCase();
if (messageType === 1) return normalizeDmChatId(fromLogin);
if (messageType === 2) return normalizeDmChatId(toLogin);
if (currentLogin && currentLogin === fromLogin.toLowerCase()) {
return normalizeDmChatId(toLogin);
}
return normalizeDmChatId(fromLogin);
}
async function ensureSessionAfterWsReconnect() {
if (!state.session.isAuthorized) return true;
if (wsSessionRestoreInFlight) return wsSessionRestoreInFlight;
@@ -1020,29 +1060,44 @@ async function init() {
const fromLogin = parsed.fromLogin || '';
const toLogin = parsed.toLogin || '';
const messageType = Number(parsed.messageType || 0);
const currentLogin = String(state.session?.login || '').trim().toLowerCase();
const chatPeerLogin = currentLogin && currentLogin === String(fromLogin || '').trim().toLowerCase()
? toLogin
: fromLogin;
const chatId = normalizeDmChatId(chatPeerLogin);
let shouldAckSessionDelivery = true;
let text = '';
let sessionLoginForRouting = String(state.session?.login || '').trim();
if (messageType === 1 || messageType === 2) {
const decryptContext = await waitForSignedDmDecryptContext();
if (!decryptContext.login || !decryptContext.storagePwd) {
shouldAckSessionDelivery = false;
addAppLogEntry({
level: 'warn',
source: 'signed-dm',
message: 'Отложена обработка DM: decrypt-контекст сессии ещё не готов',
details: { messageKey, baseKey: parsed.baseKey, messageType },
});
return;
}
sessionLoginForRouting = decryptContext.login;
try {
const decrypted = await authService.decryptSignedMessageContent({
parsed,
login: state.session?.login || '',
storagePwd: state.session?.storagePwdInMemory || '',
login: decryptContext.login,
storagePwd: decryptContext.storagePwd,
});
text = String(decrypted?.text || '');
} catch (error) {
shouldAckSessionDelivery = false;
addAppLogEntry({
level: 'warn',
source: 'signed-dm',
message: 'Не удалось расшифровать DM',
details: { messageKey, baseKey: parsed.baseKey, error: error?.message || 'unknown' },
});
return;
}
}
const chatId = resolveSignedDmChatId({
parsed,
login: sessionLoginForRouting,
});
let shouldRefreshToolbarUnread = false;
@@ -1125,15 +1180,17 @@ async function init() {
});
}
try {
await authService.ackSessionDelivery(messageKey);
} catch (error) {
addAppLogEntry({
level: 'warn',
source: 'signed-dm',
message: 'Не удалось отправить ACK доставки по сессии',
details: { messageKey, error: error?.message || 'unknown' },
});
if (shouldAckSessionDelivery) {
try {
await authService.ackSessionDelivery(messageKey);
} catch (error) {
addAppLogEntry({
level: 'warn',
source: 'signed-dm',
message: 'Не удалось отправить ACK доставки по сессии',
details: { messageKey, error: error?.message || 'unknown' },
});
}
}
const pageId = getRoute().pageId || '';
+27
View File
@@ -252,6 +252,8 @@ function createInitialState({ withStoredSession = true } = {}) {
appLog: [],
incomingDedup: {},
knownMessageKeys: {},
pendingOutgoingReadByBaseKey: {},
pendingIncomingReadByBaseKey: {},
outgoingTempSeq: 1,
notificationsTab: 'replies',
pageLabelCollapsed: false,
@@ -558,32 +560,46 @@ export function markOutgoingSent(tempId, { messageKey = '', baseKey = '' } = {})
export function markOutgoingReadByBaseKey(baseKey) {
if (!baseKey) return;
const keys = Object.keys(state.chats || {});
let matched = false;
keys.forEach((chatId) => {
const list = getChatMessages(chatId);
list.forEach((row) => {
if (row?.from !== 'out') return;
if (row.baseKey === baseKey) {
matched = true;
row.secondTick = true;
persistMessageRecord(chatId, row);
}
});
});
if (matched) {
delete state.pendingOutgoingReadByBaseKey[baseKey];
} else {
state.pendingOutgoingReadByBaseKey[baseKey] = true;
}
}
export function markIncomingReadByBaseKey(baseKey) {
if (!baseKey) return;
const keys = Object.keys(state.chats || {});
let matched = false;
keys.forEach((chatId) => {
const list = getChatMessages(chatId);
list.forEach((row) => {
if (row?.from !== 'in') return;
if (row.baseKey === baseKey) {
matched = true;
row.unread = false;
row.readReceiptSent = true;
persistMessageRecord(chatId, row);
}
});
});
if (matched) {
delete state.pendingIncomingReadByBaseKey[baseKey];
} else {
state.pendingIncomingReadByBaseKey[baseKey] = true;
}
}
export function markReadReceiptSentByBaseKey(baseKey) {
@@ -651,6 +667,15 @@ export function addSignedMessageToChat({
row.firstTick = row.from === 'out';
row.secondTick = Boolean(existing?.secondTick);
row.readReceiptSent = Boolean(existing?.readReceiptSent);
if (row.baseKey && row.from === 'out' && state.pendingOutgoingReadByBaseKey[row.baseKey]) {
row.secondTick = true;
delete state.pendingOutgoingReadByBaseKey[row.baseKey];
}
if (row.baseKey && row.from === 'in' && state.pendingIncomingReadByBaseKey[row.baseKey]) {
row.unread = false;
row.readReceiptSent = true;
delete state.pendingIncomingReadByBaseKey[row.baseKey];
}
if (existingIndex < 0) {
list.push(row);
@@ -872,6 +897,8 @@ function resetStateForSignedOut() {
state.appLog = next.appLog;
state.incomingDedup = next.incomingDedup;
state.knownMessageKeys = next.knownMessageKeys;
state.pendingOutgoingReadByBaseKey = next.pendingOutgoingReadByBaseKey;
state.pendingIncomingReadByBaseKey = next.pendingIncomingReadByBaseKey;
state.outgoingTempSeq = next.outgoingTempSeq;
state.notificationsTab = next.notificationsTab;
state.pageLabelCollapsed = next.pageLabelCollapsed;