mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-05 16:30:33 +08:00
fix(ios): ios local access limit for self-hosted workspace (#15338)
## Summary - Route iOS nbstore worker token reads through the main-thread MessagePort and skip Capacitor Auth on `/socket.io` polling so self-host WebSocket/XHR sync no longer hangs on Connect timeout. - Harden workspace `flavour:id` routing, DocSyncPeer abort/status handling, and `resetSync` so local selfhost edits push and Mac browsers can receive them. - Soften root-doc readiness waits and session-exchange throttling to keep mobile selfhost sign-in/sync stable under retries. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **New Features** * Improved workspace switching across local and remote environments, preserving workspace type and server context. * Added support for page navigation with query parameters. * Added iOS local-network permission messaging for self-hosted workspaces. * **Bug Fixes** * Improved document synchronization, reset handling, prioritized document refreshes, and retry behavior. * Prevented authentication headers and refresh attempts for socket connection requests. * Improved workspace reopening and routing when multiple workspace types share an ID. * Fixed handling of unlimited data query limits. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
@@ -22,6 +22,16 @@ export type WorkspaceSettingsRouteOptions = {
|
||||
scrollAnchor?: string;
|
||||
};
|
||||
|
||||
export type NavigateToPageOptions = Omit<NavigateOptions, 'replace'> & {
|
||||
search?: string | URLSearchParams;
|
||||
};
|
||||
|
||||
const normalizeSearch = (search?: string | URLSearchParams) => {
|
||||
const value = search?.toString();
|
||||
if (!value) return '';
|
||||
return value.startsWith('?') ? value : `?${value}`;
|
||||
};
|
||||
|
||||
export function buildWorkspaceSettingsPath(
|
||||
workspaceId: string,
|
||||
options?: WorkspaceSettingsRouteOptions
|
||||
@@ -84,11 +94,17 @@ export function useNavigateHelper() {
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
logic: RouteLogic = RouteLogic.PUSH,
|
||||
options?: NavigateToPageOptions
|
||||
) => {
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
const { search, ...navigateOptions } = options ?? {};
|
||||
return navigate(
|
||||
`/workspace/${workspaceId}/${pageId}${normalizeSearch(search)}`,
|
||||
{
|
||||
...navigateOptions,
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
@@ -176,8 +192,13 @@ export function useNavigateHelper() {
|
||||
);
|
||||
|
||||
const openPage = useCallback(
|
||||
(workspaceId: string, pageId: string, logic?: RouteLogic) => {
|
||||
return jumpToPage(workspaceId, pageId, logic);
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic?: RouteLogic,
|
||||
options?: NavigateToPageOptions
|
||||
) => {
|
||||
return jumpToPage(workspaceId, pageId, logic, options);
|
||||
},
|
||||
[jumpToPage]
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Divider, IconButton, Menu, MenuItem } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
} from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { WorkspaceAvatar } from '@affine/core/components/workspace-avatar';
|
||||
import {
|
||||
@@ -13,7 +16,6 @@ import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import {
|
||||
type WorkspaceMetadata,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -45,7 +47,7 @@ const WorkspaceItem = ({
|
||||
<li className={styles.wsItem}>
|
||||
<button className={clsx(styles.wsCard, className)} {...attrs}>
|
||||
<WorkspaceAvatar
|
||||
key={workspace.id}
|
||||
key={`${workspace.flavour}:${workspace.id}`}
|
||||
meta={workspace}
|
||||
rounded={6}
|
||||
data-testid="workspace-avatar"
|
||||
@@ -71,7 +73,7 @@ export const WorkspaceList = (props: WorkspaceListProps) => {
|
||||
|
||||
return workspaceList.map(item => (
|
||||
<WorkspaceItem
|
||||
key={item.id}
|
||||
key={`${item.flavour}:${item.id}`}
|
||||
workspace={item}
|
||||
onClick={() => props.onClick(item)}
|
||||
/>
|
||||
@@ -276,12 +278,18 @@ const AddServer = () => {
|
||||
};
|
||||
|
||||
export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const serversService = useService(ServersService);
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
|
||||
const currentWorkspaceId = useLiveData(
|
||||
globalContextService.globalContext.workspaceId.$
|
||||
);
|
||||
const currentWorkspaceFlavour = useLiveData(
|
||||
globalContextService.globalContext.workspaceFlavour.$
|
||||
);
|
||||
const servers = useLiveData(serversService.servers$);
|
||||
const affineCloudServer = useMemo(
|
||||
() => servers.find(s => s.id === 'affine-cloud') as Server,
|
||||
@@ -311,12 +319,29 @@ export const SelectorMenu = ({ onClose }: { onClose?: () => void }) => {
|
||||
const handleClickWorkspace = useCallback(
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
const id = workspaceMetadata.id;
|
||||
if (id !== currentWorkspace?.id) {
|
||||
jumpToPage(id, 'home');
|
||||
const isCurrentWorkspace =
|
||||
id === currentWorkspaceId &&
|
||||
workspaceMetadata.flavour === currentWorkspaceFlavour;
|
||||
if (!isCurrentWorkspace) {
|
||||
const server = servers.find(
|
||||
server => server.id === workspaceMetadata.flavour
|
||||
);
|
||||
if (workspaceMetadata.flavour !== 'local' && !server) {
|
||||
return;
|
||||
}
|
||||
const searchParams = new URLSearchParams({
|
||||
flavour: workspaceMetadata.flavour,
|
||||
});
|
||||
if (workspaceMetadata.flavour !== 'local' && server) {
|
||||
searchParams.set('server', server.baseUrl);
|
||||
}
|
||||
jumpToPage(id, 'home', RouteLogic.PUSH, {
|
||||
search: searchParams,
|
||||
});
|
||||
}
|
||||
onClose?.();
|
||||
},
|
||||
[currentWorkspace.id, jumpToPage, onClose]
|
||||
[currentWorkspaceFlavour, currentWorkspaceId, jumpToPage, onClose, servers]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -102,9 +102,54 @@ export const Component = () => {
|
||||
const [workspaceNotFound, setWorkspaceNotFound] = useState(false);
|
||||
const listLoading = useLiveData(workspacesService.list.isRevalidating$);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
|
||||
const serverSearchParam = searchParams.get('server');
|
||||
const flavourSearchParam = searchParams.get('flavour');
|
||||
const serverFromSearchParams = useLiveData(
|
||||
serverSearchParam
|
||||
? serversService.serverByBaseUrl$(serverSearchParam)
|
||||
: undefined
|
||||
);
|
||||
const meta = useMemo(() => {
|
||||
return workspaces.find(({ id }) => id === params.workspaceId);
|
||||
}, [workspaces, params.workspaceId]);
|
||||
const workspaceId = params.workspaceId;
|
||||
if (!workspaceId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const findByFlavour = (flavour: string) =>
|
||||
workspaces.find(
|
||||
workspace =>
|
||||
workspace.id === workspaceId && workspace.flavour === flavour
|
||||
);
|
||||
|
||||
if (flavourSearchParam) {
|
||||
return findByFlavour(flavourSearchParam);
|
||||
}
|
||||
|
||||
if (serverSearchParam) {
|
||||
if (!serverFromSearchParams) {
|
||||
return undefined;
|
||||
}
|
||||
return findByFlavour(serverFromSearchParams.id);
|
||||
}
|
||||
|
||||
const lastWorkspaceFlavour = localStorage.getItem('last_workspace_flavour');
|
||||
if (lastWorkspaceFlavour) {
|
||||
const lastWorkspace = findByFlavour(lastWorkspaceFlavour);
|
||||
if (lastWorkspace) {
|
||||
return lastWorkspace;
|
||||
}
|
||||
}
|
||||
|
||||
const matches = workspaces.filter(({ id }) => id === workspaceId);
|
||||
return matches.length === 1 ? matches[0] : undefined;
|
||||
}, [
|
||||
flavourSearchParam,
|
||||
params.workspaceId,
|
||||
serverSearchParam,
|
||||
serverFromSearchParams,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
// if listLoading is false, we can show 404 page, otherwise we should show loading page.
|
||||
useEffect(() => {
|
||||
@@ -135,19 +180,16 @@ export const Component = () => {
|
||||
return;
|
||||
}, [listLoading, meta, workspaceNotFound, workspacesService]);
|
||||
|
||||
// server search params
|
||||
const serverFromSearchParams = useLiveData(
|
||||
searchParams.has('server')
|
||||
? serversService.serverByBaseUrl$(searchParams.get('server') as string)
|
||||
: undefined
|
||||
);
|
||||
// server from workspace
|
||||
const serverFromWorkspace = useLiveData(
|
||||
meta?.flavour && meta.flavour !== 'local'
|
||||
? serversService.server$(meta?.flavour)
|
||||
: undefined
|
||||
);
|
||||
const server = serverFromWorkspace ?? serverFromSearchParams;
|
||||
const server =
|
||||
meta?.flavour === 'local'
|
||||
? undefined
|
||||
: (serverFromWorkspace ?? serverFromSearchParams);
|
||||
|
||||
if (workspaceNotFound) {
|
||||
if (
|
||||
|
||||
@@ -86,6 +86,7 @@ export const WorkspaceLayout = ({
|
||||
})
|
||||
);
|
||||
localStorage.setItem('last_workspace_id', workspace.id);
|
||||
localStorage.setItem('last_workspace_flavour', workspace.flavour);
|
||||
globalContextService.globalContext.workspaceId.set(workspace.id);
|
||||
if (workspaceServer) {
|
||||
globalContextService.globalContext.serverId.set(workspaceServer.id);
|
||||
|
||||
@@ -41,7 +41,8 @@ export function useBindWorkbenchToBrowserRouter(
|
||||
|
||||
const newBrowserLocation = viewLocationToBrowserLocation(
|
||||
update.location,
|
||||
basename
|
||||
basename,
|
||||
browserLocation.search
|
||||
);
|
||||
|
||||
navigate(newBrowserLocation, {
|
||||
@@ -97,12 +98,44 @@ function browserLocationToViewLocation(
|
||||
};
|
||||
}
|
||||
|
||||
function preserveWorkspaceContextSearch(
|
||||
nextSearch: string,
|
||||
currentSearch: string
|
||||
) {
|
||||
const nextParams = new URLSearchParams(nextSearch);
|
||||
const currentParams = new URLSearchParams(currentSearch);
|
||||
const currentFlavour = currentParams.get('flavour');
|
||||
const nextFlavour = nextParams.get('flavour');
|
||||
|
||||
if (
|
||||
!nextParams.has('flavour') &&
|
||||
!nextParams.has('server') &&
|
||||
currentFlavour
|
||||
) {
|
||||
nextParams.set('flavour', currentFlavour);
|
||||
}
|
||||
|
||||
const resolvedNextFlavour = nextParams.get('flavour');
|
||||
const shouldPreserveServer =
|
||||
resolvedNextFlavour !== 'local' &&
|
||||
(!nextFlavour || !currentFlavour || nextFlavour === currentFlavour);
|
||||
const currentServer = currentParams.get('server');
|
||||
if (!nextParams.has('server') && currentServer && shouldPreserveServer) {
|
||||
nextParams.set('server', currentServer);
|
||||
}
|
||||
|
||||
const search = nextParams.toString();
|
||||
return search ? `?${search}` : '';
|
||||
}
|
||||
|
||||
function viewLocationToBrowserLocation(
|
||||
location: Location,
|
||||
basename: string
|
||||
basename: string,
|
||||
currentSearch: string
|
||||
): Location {
|
||||
return {
|
||||
...location,
|
||||
pathname: `${basename}${location.pathname}`,
|
||||
search: preserveWorkspaceContextSearch(location.search, currentSearch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ObjectPool, Service } from '@toeverything/infra';
|
||||
|
||||
import type { Workspace } from '../entities/workspace';
|
||||
import { WorkspaceInitialized } from '../events';
|
||||
import type { WorkspaceMetadata } from '../metadata';
|
||||
import type { WorkspaceOpenOptions } from '../open-options';
|
||||
import { WorkspaceScope } from '../scopes/workspace';
|
||||
import type { WorkspaceFlavoursService } from './flavours';
|
||||
@@ -13,6 +14,9 @@ import { WorkspaceService } from './workspace';
|
||||
|
||||
const logger = new DebugLogger('affine:workspace-repository');
|
||||
|
||||
const getWorkspacePoolKey = (metadata: WorkspaceMetadata) =>
|
||||
`${metadata.flavour}:${metadata.id}`;
|
||||
|
||||
export class WorkspaceRepositoryService extends Service {
|
||||
constructor(
|
||||
private readonly flavoursService: WorkspaceFlavoursService,
|
||||
@@ -58,7 +62,7 @@ export class WorkspaceRepositoryService extends Service {
|
||||
};
|
||||
}
|
||||
|
||||
const exist = this.pool.get(options.metadata.id);
|
||||
const exist = this.pool.get(getWorkspacePoolKey(options.metadata));
|
||||
if (exist) {
|
||||
return {
|
||||
workspace: exist.obj,
|
||||
@@ -68,7 +72,7 @@ export class WorkspaceRepositoryService extends Service {
|
||||
|
||||
const workspace = this.instantiate(options, customEngineWorkerInitOptions);
|
||||
|
||||
const ref = this.pool.put(workspace.meta.id, workspace);
|
||||
const ref = this.pool.put(getWorkspacePoolKey(workspace.meta), workspace);
|
||||
|
||||
return {
|
||||
workspace: ref.obj,
|
||||
@@ -76,9 +80,13 @@ export class WorkspaceRepositoryService extends Service {
|
||||
};
|
||||
};
|
||||
|
||||
openByWorkspaceId = (workspaceId: string) => {
|
||||
const workspaceMetadata =
|
||||
this.workspacesListService.list.workspace$(workspaceId).value;
|
||||
openByWorkspaceId = (workspaceId: string, flavour?: string | null) => {
|
||||
const workspaceMetadata = flavour
|
||||
? this.workspacesListService.list.workspaces$.value.find(
|
||||
workspace =>
|
||||
workspace.id === workspaceId && workspace.flavour === flavour
|
||||
)
|
||||
: this.workspacesListService.list.workspace$(workspaceId).value;
|
||||
return workspaceMetadata && this.open({ metadata: workspaceMetadata });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user