mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(p2p): stop account recovery reload loop
This commit is contained in:
@@ -32,6 +32,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
shouldShowSnow: true,
|
shouldShowSnow: true,
|
||||||
createAccountMock: vi.fn().mockResolvedValue(undefined),
|
createAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||||
removeSnowMock: vi.fn(),
|
removeSnowMock: vi.fn(),
|
||||||
|
setActiveAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||||
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||||
replyModalState: {
|
replyModalState: {
|
||||||
activeCid: null,
|
activeCid: null,
|
||||||
@@ -52,6 +53,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
|
|
||||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||||
createAccount: () => testState.createAccountMock(),
|
createAccount: () => testState.createAccountMock(),
|
||||||
|
setActiveAccount: (accountName: string) => testState.setActiveAccountMock(accountName),
|
||||||
setAccount: (account: unknown) => testState.setAccountMock(account),
|
setAccount: (account: unknown) => testState.setAccountMock(account),
|
||||||
useAccount: () => testState.account,
|
useAccount: () => testState.account,
|
||||||
useAccounts: () => ({ accounts: testState.account ? [testState.account] : [] }),
|
useAccounts: () => ({ accounts: testState.account ? [testState.account] : [] }),
|
||||||
@@ -371,6 +373,7 @@ describe('App', () => {
|
|||||||
testState.communities = {};
|
testState.communities = {};
|
||||||
testState.useThemeMock.mockReset();
|
testState.useThemeMock.mockReset();
|
||||||
testState.createAccountMock.mockReset().mockResolvedValue(undefined);
|
testState.createAccountMock.mockReset().mockResolvedValue(undefined);
|
||||||
|
testState.setActiveAccountMock.mockReset().mockResolvedValue(undefined);
|
||||||
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
||||||
testState.closeCreateBoardModalMock.mockReset();
|
testState.closeCreateBoardModalMock.mockReset();
|
||||||
testState.initSnowMock.mockReset();
|
testState.initSnowMock.mockReset();
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
|
|||||||
const testState = vi.hoisted(() => ({
|
const testState = vi.hoisted(() => ({
|
||||||
account: undefined as Record<string, any> | undefined,
|
account: undefined as Record<string, any> | undefined,
|
||||||
accounts: [] as Record<string, any>[] | undefined,
|
accounts: [] as Record<string, any>[] | undefined,
|
||||||
|
accountsState: 'initializing',
|
||||||
createAccountMock: vi.fn().mockResolvedValue(undefined),
|
createAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||||
|
setActiveAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||||
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||||
createAccount: () => testState.createAccountMock(),
|
createAccount: () => testState.createAccountMock(),
|
||||||
setAccount: (account: unknown) => testState.setAccountMock(account),
|
setAccount: (account: unknown) => testState.setAccountMock(account),
|
||||||
|
setActiveAccount: (accountName: string) => testState.setActiveAccountMock(accountName),
|
||||||
useAccount: () => testState.account,
|
useAccount: () => testState.account,
|
||||||
useAccounts: () => ({ accounts: testState.accounts }),
|
useAccounts: () => ({ accounts: testState.accounts, state: testState.accountsState }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
@@ -48,7 +51,9 @@ describe('useBrowserPureP2PAccountUpgrade', () => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
testState.account = undefined;
|
testState.account = undefined;
|
||||||
testState.accounts = [];
|
testState.accounts = [];
|
||||||
|
testState.accountsState = 'initializing';
|
||||||
testState.createAccountMock.mockReset().mockResolvedValue(undefined);
|
testState.createAccountMock.mockReset().mockResolvedValue(undefined);
|
||||||
|
testState.setActiveAccountMock.mockReset().mockResolvedValue(undefined);
|
||||||
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
||||||
reloadMock = vi.fn();
|
reloadMock = vi.fn();
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
@@ -144,31 +149,56 @@ describe('useBrowserPureP2PAccountUpgrade', () => {
|
|||||||
expect(reloadMock).not.toHaveBeenCalled();
|
expect(reloadMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers a missing browser account after the hooks store finishes initializing', async () => {
|
it('waits for the hooks store instead of creating duplicate accounts while initialization is running', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = true;
|
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = true;
|
||||||
|
|
||||||
await renderHook();
|
await renderHook();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await vi.advanceTimersByTimeAsync(1000);
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testState.createAccountMock).not.toHaveBeenCalled();
|
expect(testState.createAccountMock).not.toHaveBeenCalled();
|
||||||
|
expect(testState.setActiveAccountMock).not.toHaveBeenCalled();
|
||||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = false;
|
expect(reloadMock).not.toHaveBeenCalled();
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(1000);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testState.createAccountMock).toHaveBeenCalledOnce();
|
|
||||||
expect(reloadMock).toHaveBeenCalledOnce();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats temporarily missing accounts as empty while recovering', async () => {
|
it('recovers an empty account store without reloading', async () => {
|
||||||
|
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = false;
|
||||||
|
|
||||||
|
await renderHook();
|
||||||
|
|
||||||
|
expect(testState.createAccountMock).toHaveBeenCalledOnce();
|
||||||
|
expect(testState.setActiveAccountMock).not.toHaveBeenCalled();
|
||||||
|
expect(reloadMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects the first existing account when no active account is selected', async () => {
|
||||||
|
testState.accountsState = 'succeeded';
|
||||||
|
testState.account = undefined;
|
||||||
|
testState.accounts = [
|
||||||
|
{
|
||||||
|
id: 'account-1',
|
||||||
|
name: 'Account 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'account-2',
|
||||||
|
name: 'Account 2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await renderHook();
|
||||||
|
|
||||||
|
expect(testState.setActiveAccountMock).toHaveBeenCalledWith('Account 1');
|
||||||
|
expect(testState.createAccountMock).not.toHaveBeenCalled();
|
||||||
|
expect(reloadMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats temporarily missing accounts as still initializing', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
testState.accounts = undefined;
|
testState.accounts = undefined;
|
||||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = false;
|
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = true;
|
||||||
|
|
||||||
await renderHook();
|
await renderHook();
|
||||||
|
|
||||||
@@ -177,7 +207,8 @@ describe('useBrowserPureP2PAccountUpgrade', () => {
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(testState.createAccountMock).toHaveBeenCalledOnce();
|
expect(testState.createAccountMock).not.toHaveBeenCalled();
|
||||||
expect(reloadMock).toHaveBeenCalledOnce();
|
expect(testState.setActiveAccountMock).not.toHaveBeenCalled();
|
||||||
|
expect(reloadMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { createAccount, setAccount, useAccount, useAccounts } from '@bitsocial/bitsocial-react-hooks';
|
import { createAccount, setAccount, setActiveAccount, useAccount, useAccounts } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import { getBrowserPureP2PAccountOptions, shouldUpgradeBrowserPureP2PAccount } from '../lib/p2p-runtime';
|
import { getBrowserPureP2PAccountOptions, shouldUpgradeBrowserPureP2PAccount } from '../lib/p2p-runtime';
|
||||||
|
|
||||||
type AccountShape = Record<string, unknown> & {
|
type AccountShape = Record<string, unknown> & {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACCOUNT_RECOVERY_CHECK_MS = 1000;
|
|
||||||
|
|
||||||
export const useBrowserPureP2PAccountUpgrade = () => {
|
export const useBrowserPureP2PAccountUpgrade = () => {
|
||||||
const account = useAccount() as AccountShape | undefined;
|
const account = useAccount() as AccountShape | undefined;
|
||||||
const { accounts = [] } = useAccounts();
|
const { accounts = [], state: accountsState } = useAccounts();
|
||||||
const recoveryStartedRef = useRef(false);
|
const activeAccountRecoveryNameRef = useRef<string | undefined>(undefined);
|
||||||
|
const missingAccountRecoveryStartedRef = useRef(false);
|
||||||
const upgradeAccountIdRef = useRef<string | undefined>(undefined);
|
const upgradeAccountIdRef = useRef<string | undefined>(undefined);
|
||||||
|
const firstAccountName = accounts[0]?.name;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (account?.id || accounts.length > 0 || recoveryStartedRef.current) return;
|
const accountsStoreInitializing = window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING === true;
|
||||||
|
|
||||||
const intervalId = window.setInterval(() => {
|
if (account?.id) return;
|
||||||
if (window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING) return;
|
|
||||||
|
|
||||||
recoveryStartedRef.current = true;
|
if (accounts.length === 0) {
|
||||||
window.clearInterval(intervalId);
|
if (accountsStoreInitializing || missingAccountRecoveryStartedRef.current) return;
|
||||||
void createAccount()
|
|
||||||
.then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
recoveryStartedRef.current = false;
|
|
||||||
console.error('Failed to recover missing browser account', error);
|
|
||||||
});
|
|
||||||
}, ACCOUNT_RECOVERY_CHECK_MS);
|
|
||||||
|
|
||||||
return () => {
|
missingAccountRecoveryStartedRef.current = true;
|
||||||
window.clearInterval(intervalId);
|
void createAccount().catch((error) => {
|
||||||
};
|
missingAccountRecoveryStartedRef.current = false;
|
||||||
}, [account?.id, accounts.length]);
|
console.error('Failed to recover missing browser account', error);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstAccountName) return;
|
||||||
|
if (activeAccountRecoveryNameRef.current === firstAccountName) return;
|
||||||
|
|
||||||
|
activeAccountRecoveryNameRef.current = firstAccountName;
|
||||||
|
void setActiveAccount(firstAccountName).catch((error) => {
|
||||||
|
activeAccountRecoveryNameRef.current = undefined;
|
||||||
|
console.error('Failed to recover missing active browser account', error);
|
||||||
|
});
|
||||||
|
}, [account?.id, accounts.length, accountsState, firstAccountName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!account?.id || !shouldUpgradeBrowserPureP2PAccount(account)) return;
|
if (!account?.id || !shouldUpgradeBrowserPureP2PAccount(account)) return;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_HTTP_ROUTER_URLS } from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts/account-generator.js';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +18,7 @@ const createStorage = (values: Record<string, string | undefined> = {}) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('p2p-browser-config', () => {
|
describe('p2p-browser-config', () => {
|
||||||
const defaultHttpRouters = ['https://peers.plebpubsub.xyz', 'https://routing.lol', 'https://peers.pleb.bot'];
|
const defaultHttpRouters = DEFAULT_HTTP_ROUTER_URLS;
|
||||||
|
|
||||||
it('configures browser PKC options for gateway mode by default', () => {
|
it('configures browser PKC options for gateway mode by default', () => {
|
||||||
const chainProviders = {
|
const chainProviders = {
|
||||||
@@ -37,6 +38,7 @@ describe('p2p-browser-config', () => {
|
|||||||
expect(targetWindow.defaultPkcOptions).toEqual({
|
expect(targetWindow.defaultPkcOptions).toEqual({
|
||||||
chainProviders,
|
chainProviders,
|
||||||
...getBrowserGatewayPkcOptions(),
|
...getBrowserGatewayPkcOptions(),
|
||||||
|
httpRoutersOptions: defaultHttpRouters,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ describe('p2p-browser-config', () => {
|
|||||||
expect(targetWindow.defaultPkcOptions).toEqual({
|
expect(targetWindow.defaultPkcOptions).toEqual({
|
||||||
chainProviders,
|
chainProviders,
|
||||||
...getBrowserGatewayPkcOptions(),
|
...getBrowserGatewayPkcOptions(),
|
||||||
|
httpRoutersOptions: defaultHttpRouters,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ export const PURE_P2P_BROWSER_SETTING_KEY = '5chan:pure-p2p-browser-enabled';
|
|||||||
export const BROWSER_PURE_P2P_DEFAULT_ENABLED = false;
|
export const BROWSER_PURE_P2P_DEFAULT_ENABLED = false;
|
||||||
|
|
||||||
const BROWSER_PUBSUB_KUBO_RPC_CLIENTS_OPTIONS = ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'];
|
const BROWSER_PUBSUB_KUBO_RPC_CLIENTS_OPTIONS = ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'];
|
||||||
|
// Keep this aligned with bitsocial-react-hooks' DEFAULT_HTTP_ROUTER_URLS without relying on a package-internal runtime import before window.defaultPkcOptions is configured.
|
||||||
|
const DEFAULT_HTTP_ROUTER_URLS = [
|
||||||
|
'https://peers.pleb.bot',
|
||||||
|
'https://routing.lol',
|
||||||
|
'https://peers.forumindex.com',
|
||||||
|
'https://peers.plebpubsub.xyz',
|
||||||
|
'https://routerofbitsocial.xyz',
|
||||||
|
'https://bsotracker.online',
|
||||||
|
];
|
||||||
|
|
||||||
export const P2P_BROWSER_PKC_OPTIONS = {
|
export const P2P_BROWSER_PKC_OPTIONS = {
|
||||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||||
@@ -9,7 +18,7 @@ export const P2P_BROWSER_PKC_OPTIONS = {
|
|||||||
kuboRpcClientsOptions: undefined,
|
kuboRpcClientsOptions: undefined,
|
||||||
pubsubHttpClientsOptions: undefined,
|
pubsubHttpClientsOptions: undefined,
|
||||||
pubsubKuboRpcClientsOptions: undefined as string[] | undefined,
|
pubsubKuboRpcClientsOptions: undefined as string[] | undefined,
|
||||||
httpRoutersOptions: ['https://peers.plebpubsub.xyz', 'https://routing.lol', 'https://peers.pleb.bot'],
|
httpRoutersOptions: DEFAULT_HTTP_ROUTER_URLS,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GATEWAY_BROWSER_PKC_OPTIONS = {
|
const GATEWAY_BROWSER_PKC_OPTIONS = {
|
||||||
@@ -18,7 +27,7 @@ const GATEWAY_BROWSER_PKC_OPTIONS = {
|
|||||||
libp2pJsClientsOptions: undefined,
|
libp2pJsClientsOptions: undefined,
|
||||||
pubsubHttpClientsOptions: undefined,
|
pubsubHttpClientsOptions: undefined,
|
||||||
pubsubKuboRpcClientsOptions: BROWSER_PUBSUB_KUBO_RPC_CLIENTS_OPTIONS,
|
pubsubKuboRpcClientsOptions: BROWSER_PUBSUB_KUBO_RPC_CLIENTS_OPTIONS,
|
||||||
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
|
httpRoutersOptions: DEFAULT_HTTP_ROUTER_URLS,
|
||||||
};
|
};
|
||||||
|
|
||||||
type P2PBrowserConfigWindow = {
|
type P2PBrowserConfigWindow = {
|
||||||
|
|||||||
Reference in New Issue
Block a user