Исправить 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 String MAGIC = "SHiNE";
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 1;
private static final int ARCHIVE_HEAD_PAYLOAD_BYTES = 64;
private SolanaUserPdaImportService() {}
@@ -266,6 +267,8 @@ public final class SolanaUserPdaImportService {
}
} else if (blockType == 70) {
c += 1;
} else if (blockType == 100) {
c += ARCHIVE_HEAD_PAYLOAD_BYTES;
} else {
return null;
}
@@ -374,6 +377,8 @@ public final class SolanaUserPdaImportService {
}
} else if (blockType == 70) {
c += 1;
} else if (blockType == 100) {
c += ARCHIVE_HEAD_PAYLOAD_BYTES;
} else {
return null;
}
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.12.15
server.version=1.10.6
client.version=1.12.16
server.version=1.10.7
+60 -39
View File
@@ -306,6 +306,7 @@ function createChannelReadTracker({
let persistedSeenCount = safeInitialSeenCount;
let initialPersistPending = !!initializeIfMissing;
let inFlight = false;
let writeBlocked = false;
let disposed = false;
let rafId = 0;
let timerId = 0;
@@ -318,7 +319,7 @@ function createChannelReadTracker({
};
const queueFlush = (delayMs = 180) => {
if (disposed || !canWrite) return;
if (disposed || writeBlocked || !canWrite) return;
clearTimer();
timerId = setTimeout(() => {
timerId = 0;
@@ -327,7 +328,7 @@ function createChannelReadTracker({
};
const flush = async () => {
if (disposed || !canWrite) return;
if (disposed || writeBlocked || !canWrite) return;
const next = Math.max(safeInitialSeenCount, Math.min(desiredSeenCount, safeMessagesCount));
if (next <= persistedSeenCount && !initialPersistPending) return;
if (inFlight) {
@@ -358,6 +359,18 @@ function createChannelReadTracker({
});
}
} 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 });
queueFlush(800);
} finally {
@@ -2411,46 +2424,54 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
? window.setTimeout(() => scrollChannelToUnreadLine(screen, unreadCount, false), 40)
: 0;
const tracker = createChannelReadTracker({
screen,
routeKey,
ownerBlockchainName: channelData.channel?.ownerBlockchainName || channelData.selector?.ownerBlockchainName,
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;
const shouldTrackReadState = !!(
channelData.isSubscribed
&& !channelData.isOwnChannel
&& !channelData.isDiary
);
// The "Новые сообщения" divider is a snapshot of the unread boundary at the
// moment this channel view was opened. Persisting read state must not move or
// remove it during the current view session; reopening the channel recalculates it.
const tracker = shouldTrackReadState
? createChannelReadTracker({
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'];
for (const group of feedGroups) {
const rows = Array.isArray(state.channelsFeed?.[group]) ? state.channelsFeed[group] : [];
const row = rows.find((item) => (
String(item?.channel?.ownerBlockchainName || '') === String(channelData.selector?.ownerBlockchainName || '')
&& Number(item?.channel?.channelRoot?.blockNumber) === Number(channelData.selector?.channelRootBlockNumber)
&& normalizeRouteHash(item?.channel?.channelRoot?.blockHash) === normalizeRouteHash(channelData.selector?.channelRootBlockHash)
));
if (row) {
row.readCount = persistedReadCount;
row.unreadCount = persistedUnreadCount;
row.readStateInitialized = true;
// The "Новые сообщения" divider is a snapshot of the unread boundary at the
// moment this channel view was opened. Persisting read state must not move or
// remove it during the current view session; reopening the channel recalculates it.
const feedGroups = ['ownedChannels', 'followedUsersChannels', 'followedChannels'];
for (const group of feedGroups) {
const rows = Array.isArray(state.channelsFeed?.[group]) ? state.channelsFeed[group] : [];
const row = rows.find((item) => (
String(item?.channel?.ownerBlockchainName || '') === String(channelData.selector?.ownerBlockchainName || '')
&& Number(item?.channel?.channelRoot?.blockNumber) === Number(channelData.selector?.channelRootBlockNumber)
&& normalizeRouteHash(item?.channel?.channelRoot?.blockHash) === normalizeRouteHash(channelData.selector?.channelRootBlockHash)
));
if (row) {
row.readCount = persistedReadCount;
row.unreadCount = persistedUnreadCount;
row.readStateInitialized = true;
}
}
}
showStatus('');
},
});
showStatus('');
},
})
: { cleanup() {}, measure() {} };
return () => {
if (pendingScrollTimer) window.clearTimeout(pendingScrollTimer);