Исправить read-state каналов и импорт Solana PDA

This commit is contained in:
AidarKC
2026-09-17 21:10:48 +03:00
parent 1be1d56599
commit 7b81d140ec
3 changed files with 67 additions and 41 deletions
@@ -28,6 +28,7 @@ public final class SolanaUserPdaImportService {
private static final HttpClient HTTP = HttpClient.newHttpClient(); private static final HttpClient HTTP = HttpClient.newHttpClient();
private static final String MAGIC = "SHiNE"; private static final String MAGIC = "SHiNE";
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 1; private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 1;
private static final int ARCHIVE_HEAD_PAYLOAD_BYTES = 64;
private SolanaUserPdaImportService() {} private SolanaUserPdaImportService() {}
@@ -266,6 +267,8 @@ public final class SolanaUserPdaImportService {
} }
} else if (blockType == 70) { } else if (blockType == 70) {
c += 1; c += 1;
} else if (blockType == 100) {
c += ARCHIVE_HEAD_PAYLOAD_BYTES;
} else { } else {
return null; return null;
} }
@@ -374,6 +377,8 @@ public final class SolanaUserPdaImportService {
} }
} else if (blockType == 70) { } else if (blockType == 70) {
c += 1; c += 1;
} else if (blockType == 100) {
c += ARCHIVE_HEAD_PAYLOAD_BYTES;
} else { } else {
return null; return null;
} }
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.12.15 client.version=1.12.16
server.version=1.10.6 server.version=1.10.7
+60 -39
View File
@@ -306,6 +306,7 @@ function createChannelReadTracker({
let persistedSeenCount = safeInitialSeenCount; let persistedSeenCount = safeInitialSeenCount;
let initialPersistPending = !!initializeIfMissing; let initialPersistPending = !!initializeIfMissing;
let inFlight = false; let inFlight = false;
let writeBlocked = false;
let disposed = false; let disposed = false;
let rafId = 0; let rafId = 0;
let timerId = 0; let timerId = 0;
@@ -318,7 +319,7 @@ function createChannelReadTracker({
}; };
const queueFlush = (delayMs = 180) => { const queueFlush = (delayMs = 180) => {
if (disposed || !canWrite) return; if (disposed || writeBlocked || !canWrite) return;
clearTimer(); clearTimer();
timerId = setTimeout(() => { timerId = setTimeout(() => {
timerId = 0; timerId = 0;
@@ -327,7 +328,7 @@ function createChannelReadTracker({
}; };
const flush = async () => { const flush = async () => {
if (disposed || !canWrite) return; if (disposed || writeBlocked || !canWrite) return;
const next = Math.max(safeInitialSeenCount, Math.min(desiredSeenCount, safeMessagesCount)); const next = Math.max(safeInitialSeenCount, Math.min(desiredSeenCount, safeMessagesCount));
if (next <= persistedSeenCount && !initialPersistPending) return; if (next <= persistedSeenCount && !initialPersistPending) return;
if (inFlight) { if (inFlight) {
@@ -358,6 +359,18 @@ function createChannelReadTracker({
}); });
} }
} catch (error) { } catch (error) {
const errorCode = String(error?.code || '').trim().toUpperCase();
if (errorCode === 'CHANNEL_NOT_FOLLOWED') {
// This is a persistent state mismatch, not a transient transport failure.
// Stop writes for the current channel view instead of hammering the server
// after every scroll/resize. A fresh view after subscribe/refresh will create
// a new tracker from the latest subscription state.
writeBlocked = true;
initialPersistPending = false;
clearTimer();
if (typeof onPersistError === 'function') onPersistError(error, { retrying: false });
return;
}
if (typeof onPersistError === 'function') onPersistError(error, { retrying: true }); if (typeof onPersistError === 'function') onPersistError(error, { retrying: true });
queueFlush(800); queueFlush(800);
} finally { } finally {
@@ -2411,46 +2424,54 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
? window.setTimeout(() => scrollChannelToUnreadLine(screen, unreadCount, false), 40) ? window.setTimeout(() => scrollChannelToUnreadLine(screen, unreadCount, false), 40)
: 0; : 0;
const tracker = createChannelReadTracker({ const shouldTrackReadState = !!(
screen, channelData.isSubscribed
routeKey, && !channelData.isOwnChannel
ownerBlockchainName: channelData.channel?.ownerBlockchainName || channelData.selector?.ownerBlockchainName, && !channelData.isDiary
channelName: channelData.channel?.name || channelData.channel?.channelName, );
initializeIfMissing: !!(channelData.isSubscribed && !channelData.readStateInitialized),
unreadCount,
messagesCount,
initialSeenCount: readCount,
onPersistError: (error, options = {}) => {
const detail = String(error?.message || '').trim();
const retryText = options?.retrying === false ? '' : ' Сервер повторит попытку автоматически.';
showStatus(`Не удалось сохранить, сколько сообщений прочитано.${detail ? ` ${detail}` : ''}${retryText}`);
},
onPersistSuccess: ({ readCount: persistedReadCount, unreadCount: persistedUnreadCount }) => {
channelData.readCount = persistedReadCount;
channelData.unreadCount = persistedUnreadCount;
channelData.readStateInitialized = true;
// The "Новые сообщения" divider is a snapshot of the unread boundary at the const tracker = shouldTrackReadState
// moment this channel view was opened. Persisting read state must not move or ? createChannelReadTracker({
// remove it during the current view session; reopening the channel recalculates it. screen,
routeKey,
ownerBlockchainName: channelData.channel?.ownerBlockchainName || channelData.selector?.ownerBlockchainName,
channelName: channelData.channel?.name || channelData.channel?.channelName,
initializeIfMissing: !!(!channelData.readStateInitialized),
unreadCount,
messagesCount,
initialSeenCount: readCount,
onPersistError: (error, options = {}) => {
const detail = String(error?.message || '').trim();
const retryText = options?.retrying === false ? '' : ' Сервер повторит попытку автоматически.';
showStatus(`Не удалось сохранить, сколько сообщений прочитано.${detail ? ` ${detail}` : ''}${retryText}`);
},
onPersistSuccess: ({ readCount: persistedReadCount, unreadCount: persistedUnreadCount }) => {
channelData.readCount = persistedReadCount;
channelData.unreadCount = persistedUnreadCount;
channelData.readStateInitialized = true;
const feedGroups = ['ownedChannels', 'followedUsersChannels', 'followedChannels']; // The "Новые сообщения" divider is a snapshot of the unread boundary at the
for (const group of feedGroups) { // moment this channel view was opened. Persisting read state must not move or
const rows = Array.isArray(state.channelsFeed?.[group]) ? state.channelsFeed[group] : []; // remove it during the current view session; reopening the channel recalculates it.
const row = rows.find((item) => (
String(item?.channel?.ownerBlockchainName || '') === String(channelData.selector?.ownerBlockchainName || '') const feedGroups = ['ownedChannels', 'followedUsersChannels', 'followedChannels'];
&& Number(item?.channel?.channelRoot?.blockNumber) === Number(channelData.selector?.channelRootBlockNumber) for (const group of feedGroups) {
&& normalizeRouteHash(item?.channel?.channelRoot?.blockHash) === normalizeRouteHash(channelData.selector?.channelRootBlockHash) const rows = Array.isArray(state.channelsFeed?.[group]) ? state.channelsFeed[group] : [];
)); const row = rows.find((item) => (
if (row) { String(item?.channel?.ownerBlockchainName || '') === String(channelData.selector?.ownerBlockchainName || '')
row.readCount = persistedReadCount; && Number(item?.channel?.channelRoot?.blockNumber) === Number(channelData.selector?.channelRootBlockNumber)
row.unreadCount = persistedUnreadCount; && normalizeRouteHash(item?.channel?.channelRoot?.blockHash) === normalizeRouteHash(channelData.selector?.channelRootBlockHash)
row.readStateInitialized = true; ));
if (row) {
row.readCount = persistedReadCount;
row.unreadCount = persistedUnreadCount;
row.readStateInitialized = true;
}
} }
} showStatus('');
showStatus(''); },
}, })
}); : { cleanup() {}, measure() {} };
return () => { return () => {
if (pendingScrollTimer) window.clearTimeout(pendingScrollTimer); if (pendingScrollTimer) window.clearTimeout(pendingScrollTimer);