mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(pubsub): repair browser pure p2p publishing
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
diff --git a/dist/browser/helia/helia-for-pkc.js b/dist/browser/helia/helia-for-pkc.js
|
||||
index e6821667b0601ac56a850e989bfedf76c14796a2..1a2ae7ab9b0afc605c9b4dcddccf25ac02aa1337 100644
|
||||
index e6821667b0601ac56a850e989bfedf76c14796a2..87d84a502bdbf72b8cf22b7a8ac6bd68e542c611 100644
|
||||
--- a/dist/browser/helia/helia-for-pkc.js
|
||||
+++ b/dist/browser/helia/helia-for-pkc.js
|
||||
@@ -145,6 +145,11 @@ export async function createLibp2pJsClientOrUseExistingOne(pkcOptions) {
|
||||
@@ -45,7 +45,7 @@ index e6821667b0601ac56a850e989bfedf76c14796a2..1a2ae7ab9b0afc605c9b4dcddccf25ac
|
||||
unsubscribe: async (topic, handler, options) => {
|
||||
throwIfHeliaIsStoppingOrStopped();
|
||||
diff --git a/dist/browser/publications/publication.js b/dist/browser/publications/publication.js
|
||||
index 5e17c02a39c2715c7ac932695fe90d5078bce405..2d43d996e2b3cdab759e89c296eb9ed8aae857db 100644
|
||||
index 5e17c02a39c2715c7ac932695fe90d5078bce405..8c61955ee6ef6dd353e57584b4aaf745e143f6d4 100644
|
||||
--- a/dist/browser/publications/publication.js
|
||||
+++ b/dist/browser/publications/publication.js
|
||||
@@ -842,8 +842,14 @@ class Publication extends TypedEmitter {
|
||||
|
||||
@@ -30,7 +30,9 @@ const testState = vi.hoisted(() => ({
|
||||
isMobile: false,
|
||||
isSpecialEnabled: false,
|
||||
shouldShowSnow: true,
|
||||
createAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||
removeSnowMock: vi.fn(),
|
||||
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||
replyModalState: {
|
||||
activeCid: null,
|
||||
closeModal: vi.fn(),
|
||||
@@ -49,7 +51,10 @@ const testState = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||
createAccount: () => testState.createAccountMock(),
|
||||
setAccount: (account: unknown) => testState.setAccountMock(account),
|
||||
useAccount: () => testState.account,
|
||||
useAccounts: () => ({ accounts: testState.account ? [testState.account] : [] }),
|
||||
useAccountComment: ({ commentIndex }: { commentIndex?: number }) => (typeof commentIndex === 'number' ? testState.accountComments[commentIndex] : undefined),
|
||||
useCommunity: (options?: { communityAddress?: string; community?: { name?: string; publicKey?: string } }) => {
|
||||
const communityAddress = options?.communityAddress ?? options?.community?.name ?? options?.community?.publicKey;
|
||||
@@ -365,6 +370,8 @@ describe('App', () => {
|
||||
testState.isDirectoryCandidate = false;
|
||||
testState.communities = {};
|
||||
testState.useThemeMock.mockReset();
|
||||
testState.createAccountMock.mockReset().mockResolvedValue(undefined);
|
||||
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
||||
testState.closeCreateBoardModalMock.mockReset();
|
||||
testState.initSnowMock.mockReset();
|
||||
testState.removeSnowMock.mockReset();
|
||||
|
||||
@@ -13,6 +13,7 @@ import useIsMobile from './hooks/use-is-mobile';
|
||||
import { useAccountCommunityAddresses } from './hooks/use-account-community-addresses';
|
||||
import useTheme from './hooks/use-theme';
|
||||
import { useDirectories } from './hooks/use-directories';
|
||||
import { useBrowserPureP2PAccountUpgrade } from './hooks/use-browser-pure-p2p-account-upgrade';
|
||||
import { useCommunityIdentifier } from './hooks/use-community-identifiers';
|
||||
import { useResolvedCommunityAddress, useResolvedDirectoryBoardPath } from './hooks/use-resolved-community-address';
|
||||
import useSafeAccountComment from './hooks/use-safe-account-comment';
|
||||
@@ -281,6 +282,8 @@ const ModQueueRoute = () => {
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
useBrowserPureP2PAccountUpgrade();
|
||||
|
||||
// Feed routes are always rendered by FeedCacheContainer (Virtuoso used for all modes)
|
||||
const boardFeedElement = null;
|
||||
const catalogFeedElement = null;
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING?: boolean;
|
||||
isElectron: boolean;
|
||||
defaultPkcOptions?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import * as React from 'react';
|
||||
import { createElement } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
account: undefined as Record<string, any> | undefined,
|
||||
accounts: [] as Record<string, any>[] | undefined,
|
||||
createAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||
setAccountMock: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||
createAccount: () => testState.createAccountMock(),
|
||||
setAccount: (account: unknown) => testState.setAccountMock(account),
|
||||
useAccount: () => testState.account,
|
||||
useAccounts: () => ({ accounts: testState.accounts }),
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
const originalLocation = window.location;
|
||||
let reloadMock: ReturnType<typeof vi.fn>;
|
||||
let root: Root;
|
||||
|
||||
const loadHook = async () => (await import('../use-browser-pure-p2p-account-upgrade')).useBrowserPureP2PAccountUpgrade;
|
||||
|
||||
const TestComponent = ({ useUpgrade }: { useUpgrade: () => void }) => {
|
||||
useUpgrade();
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderHook = async () => {
|
||||
const useUpgrade = await loadHook();
|
||||
await act(async () => {
|
||||
root.render(createElement(TestComponent, { useUpgrade }));
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
describe('useBrowserPureP2PAccountUpgrade', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
testState.account = undefined;
|
||||
testState.accounts = [];
|
||||
testState.createAccountMock.mockReset().mockResolvedValue(undefined);
|
||||
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
|
||||
reloadMock = vi.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: {
|
||||
...originalLocation,
|
||||
hostname: '5chan.app',
|
||||
reload: reloadMock,
|
||||
},
|
||||
});
|
||||
window.electronApi = undefined;
|
||||
window.isElectron = false;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
if (root) {
|
||||
act(() => root.unmount());
|
||||
}
|
||||
container?.remove();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: originalLocation,
|
||||
});
|
||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = undefined;
|
||||
window.electronApi = undefined;
|
||||
window.isElectron = false;
|
||||
});
|
||||
|
||||
it('upgrades stale gateway browser accounts and reloads after saving', async () => {
|
||||
testState.account = {
|
||||
id: 'account-1',
|
||||
name: 'Account 1',
|
||||
pkcOptions: {
|
||||
httpRoutersOptions: ['https://router.old.example'],
|
||||
ipfsGatewayUrls: ['https://gateway.old.example'],
|
||||
pubsubKuboRpcClientsOptions: ['https://pubsub.old.example/api/v0'],
|
||||
},
|
||||
};
|
||||
testState.accounts = [testState.account];
|
||||
|
||||
await renderHook();
|
||||
|
||||
expect(testState.setAccountMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'account-1',
|
||||
pkcOptions: expect.objectContaining({
|
||||
ipfsGatewayUrls: undefined,
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
pkcRpcClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(reloadMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not upgrade browser full-node accounts', async () => {
|
||||
testState.account = {
|
||||
id: 'account-1',
|
||||
name: 'Account 1',
|
||||
pkcOptions: {
|
||||
pkcRpcClientsOptions: ['ws://node.example/key'],
|
||||
},
|
||||
};
|
||||
testState.accounts = [testState.account];
|
||||
|
||||
await renderHook();
|
||||
|
||||
expect(testState.setAccountMock).not.toHaveBeenCalled();
|
||||
expect(reloadMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('recovers a missing browser account after the hooks store finishes initializing', async () => {
|
||||
vi.useFakeTimers();
|
||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = true;
|
||||
|
||||
await renderHook();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
});
|
||||
expect(testState.createAccountMock).not.toHaveBeenCalled();
|
||||
|
||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = false;
|
||||
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 () => {
|
||||
vi.useFakeTimers();
|
||||
testState.accounts = undefined;
|
||||
window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING = false;
|
||||
|
||||
await renderHook();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(testState.createAccountMock).toHaveBeenCalledOnce();
|
||||
expect(reloadMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { createAccount, setAccount, useAccount, useAccounts } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { getBrowserPureP2PAccountOptions, shouldUpgradeBrowserPureP2PAccount } from '../lib/p2p-runtime';
|
||||
|
||||
type AccountShape = Record<string, unknown> & {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const ACCOUNT_RECOVERY_CHECK_MS = 1000;
|
||||
|
||||
export const useBrowserPureP2PAccountUpgrade = () => {
|
||||
const account = useAccount() as AccountShape | undefined;
|
||||
const { accounts = [] } = useAccounts();
|
||||
const recoveryStartedRef = useRef(false);
|
||||
const upgradeAccountIdRef = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (account?.id || accounts.length > 0 || recoveryStartedRef.current) return;
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
if (window.BITSOCIAL_REACT_HOOKS_ACCOUNTS_STORE_INITIALIZING) return;
|
||||
|
||||
recoveryStartedRef.current = true;
|
||||
window.clearInterval(intervalId);
|
||||
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 () => {
|
||||
window.clearInterval(intervalId);
|
||||
};
|
||||
}, [account?.id, accounts.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!account?.id || !shouldUpgradeBrowserPureP2PAccount(account)) return;
|
||||
if (upgradeAccountIdRef.current === account.id) return;
|
||||
|
||||
upgradeAccountIdRef.current = account.id;
|
||||
|
||||
void setAccount({
|
||||
...account,
|
||||
pkcOptions: getBrowserPureP2PAccountOptions(account),
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
})
|
||||
.catch((error) => {
|
||||
upgradeAccountIdRef.current = undefined;
|
||||
console.error('Failed to upgrade browser account to pure P2P options', error);
|
||||
});
|
||||
}, [account]);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getP2PRuntimeMode,
|
||||
isBrowserPureP2PEnabled,
|
||||
shouldShowP2PSettingsSection,
|
||||
shouldUpgradeBrowserPureP2PAccount,
|
||||
} from '../p2p-runtime';
|
||||
|
||||
const browserWindow = {
|
||||
@@ -91,6 +92,20 @@ describe('p2p-runtime', () => {
|
||||
expect(shouldShowP2PSettingsSection(account, browserWindowWithDisabledPureP2P)).toBe(true);
|
||||
});
|
||||
|
||||
it('upgrades only stale gateway browser accounts when pure p2p is enabled', () => {
|
||||
const gatewayAccount = { pkcOptions: { ipfsGatewayUrls: ['https://gateway.example'] } };
|
||||
const browserAccount = { pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }] } };
|
||||
const mixedBrowserAccount = { pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }], pubsubKuboRpcClientsOptions: ['https://pubsub.example/api/v0'] } };
|
||||
const fullNodeAccount = { pkcOptions: { pkcRpcClientsOptions: ['ws://node.example'] } };
|
||||
|
||||
expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindow)).toBe(true);
|
||||
expect(shouldUpgradeBrowserPureP2PAccount(browserAccount, browserWindow)).toBe(false);
|
||||
expect(shouldUpgradeBrowserPureP2PAccount(mixedBrowserAccount, browserWindow)).toBe(true);
|
||||
expect(shouldUpgradeBrowserPureP2PAccount(fullNodeAccount, browserWindow)).toBe(false);
|
||||
expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithDisabledPureP2P)).toBe(false);
|
||||
expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, electronWindow)).toBe(false);
|
||||
});
|
||||
|
||||
it('builds browser p2p and gateway account options without a direct pkc-js import', () => {
|
||||
const account = {
|
||||
pkcOptions: {
|
||||
@@ -104,6 +119,7 @@ describe('p2p-runtime', () => {
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
ipfsGatewayUrls: undefined,
|
||||
pkcRpcClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: undefined,
|
||||
});
|
||||
expect(getBrowserGatewayAccountOptions(account)).toMatchObject({
|
||||
httpRoutersOptions: ['https://custom-router.example'],
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
export const PURE_P2P_BROWSER_SETTING_KEY = '5chan:pure-p2p-browser-enabled';
|
||||
export const BROWSER_PURE_P2P_DEFAULT_ENABLED = true;
|
||||
|
||||
const BROWSER_PUBSUB_KUBO_RPC_CLIENTS_OPTIONS = ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'];
|
||||
|
||||
export const P2P_BROWSER_PKC_OPTIONS = {
|
||||
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
|
||||
ipfsGatewayUrls: undefined,
|
||||
kuboRpcClientsOptions: undefined,
|
||||
pubsubHttpClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: undefined as string[] | undefined,
|
||||
httpRoutersOptions: ['https://peers.plebpubsub.xyz', 'https://routing.lol', 'https://peers.pleb.bot'],
|
||||
};
|
||||
|
||||
@@ -15,7 +17,7 @@ const GATEWAY_BROWSER_PKC_OPTIONS = {
|
||||
kuboRpcClientsOptions: undefined,
|
||||
libp2pJsClientsOptions: undefined,
|
||||
pubsubHttpClientsOptions: undefined,
|
||||
pubsubKuboRpcClientsOptions: ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'],
|
||||
pubsubKuboRpcClientsOptions: BROWSER_PUBSUB_KUBO_RPC_CLIENTS_OPTIONS,
|
||||
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
|
||||
};
|
||||
|
||||
@@ -27,9 +29,12 @@ type P2PBrowserConfigWindow = {
|
||||
localStorage?: Pick<Storage, 'getItem' | 'setItem'>;
|
||||
};
|
||||
|
||||
const cloneArray = <T>(value: T[] | undefined) => (value ? [...value] : undefined);
|
||||
|
||||
export const getBrowserPureP2PPkcOptions = () => ({
|
||||
...P2P_BROWSER_PKC_OPTIONS,
|
||||
libp2pJsClientsOptions: P2P_BROWSER_PKC_OPTIONS.libp2pJsClientsOptions.map((options) => ({ ...options })),
|
||||
pubsubKuboRpcClientsOptions: cloneArray(P2P_BROWSER_PKC_OPTIONS.pubsubKuboRpcClientsOptions),
|
||||
httpRoutersOptions: [...P2P_BROWSER_PKC_OPTIONS.httpRoutersOptions],
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ const hasArrayItems = (value: unknown) => Array.isArray(value) && value.length >
|
||||
|
||||
const hasObjectItems = (value: unknown) => !!value && typeof value === 'object' && Object.keys(value).length > 0;
|
||||
|
||||
const hasMixedBrowserPureP2POptions = (protocolOptions: AccountProtocolOptions | undefined) =>
|
||||
hasArrayItems(protocolOptions?.libp2pJsClientsOptions) &&
|
||||
(hasArrayItems(protocolOptions?.kuboRpcClientsOptions) || hasArrayItems(protocolOptions?.pubsubKuboRpcClientsOptions));
|
||||
|
||||
export const getP2PRuntimeMode = (account?: unknown, targetWindow: Window = window): P2PRuntimeMode | null => {
|
||||
const accountShape = toAccountShape(account);
|
||||
const protocolOptions = accountShape?.pkcOptions;
|
||||
@@ -66,6 +70,13 @@ export const getBrowserPureP2PAccountOptions = (account?: unknown) => ({
|
||||
pkcRpcClientsOptions: undefined,
|
||||
});
|
||||
|
||||
export const shouldUpgradeBrowserPureP2PAccount = (account?: unknown, targetWindow: Window = window) => {
|
||||
if (!account || typeof account !== 'object' || !canConfigureBrowserPureP2P(targetWindow) || !isBrowserPureP2PEnabled(account, targetWindow)) return false;
|
||||
|
||||
const protocolOptions = toAccountShape(account)?.pkcOptions;
|
||||
return getP2PRuntimeMode(account, targetWindow) === null || hasMixedBrowserPureP2POptions(protocolOptions);
|
||||
};
|
||||
|
||||
export const getBrowserGatewayAccountOptions = (account?: unknown) => {
|
||||
const protocolOptions = toAccountShape(account)?.pkcOptions;
|
||||
const gatewayOptions = getBrowserGatewayPkcOptions();
|
||||
|
||||
@@ -6272,7 +6272,7 @@ __metadata:
|
||||
|
||||
"@pkcprotocol/pkc-js@patch:@pkcprotocol/pkc-js@npm%3A0.0.47#~/.yarn/patches/@pkcprotocol-pkc-js-npm-0.0.47-e2131d72c8.patch":
|
||||
version: 0.0.47
|
||||
resolution: "@pkcprotocol/pkc-js@patch:@pkcprotocol/pkc-js@npm%3A0.0.47#~/.yarn/patches/@pkcprotocol-pkc-js-npm-0.0.47-e2131d72c8.patch::version=0.0.47&hash=df4e0a"
|
||||
resolution: "@pkcprotocol/pkc-js@patch:@pkcprotocol/pkc-js@npm%3A0.0.47#~/.yarn/patches/@pkcprotocol-pkc-js-npm-0.0.47-e2131d72c8.patch::version=0.0.47&hash=bbfad6"
|
||||
dependencies:
|
||||
"@enhances/with-resolvers": "npm:0.0.5"
|
||||
"@helia/block-brokers": "npm:5.2.4"
|
||||
|
||||
Reference in New Issue
Block a user