fix: pages loading repeatedly after reconnect (#4539)
* fix: loading bug resolved * fix: better state management while disconnecting and reconnecting * fix: connected state after synced
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { LIVE_BASE_PATH, LIVE_BASE_URL } from "@plane/constants";
|
||||
import { CollaborativeDocumentEditorWithRef } from "@plane/editor";
|
||||
import type {
|
||||
CollaborationState,
|
||||
EditorRefApi,
|
||||
EditorTitleRefApi,
|
||||
TAIMenuProps,
|
||||
@@ -57,7 +57,6 @@ type Props = {
|
||||
config: TEditorBodyConfig;
|
||||
editorReady: boolean;
|
||||
editorForwardRef: React.RefObject<EditorRefApi>;
|
||||
handleConnectionStatus: Dispatch<SetStateAction<boolean>>;
|
||||
handleEditorReady: (status: boolean) => void;
|
||||
handleOpenNavigationPane: () => void;
|
||||
handlers: TEditorBodyHandlers;
|
||||
@@ -71,14 +70,13 @@ type Props = {
|
||||
// Extended editor extensions configuration
|
||||
extendedEditorProps: TExtendedEditorExtensionsConfig;
|
||||
isFetchingFallbackBinary?: boolean;
|
||||
hasServerConnectionFailed?: boolean;
|
||||
onCollaborationStateChange?: (state: CollaborationState) => void;
|
||||
};
|
||||
|
||||
export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
config,
|
||||
editorForwardRef,
|
||||
handleConnectionStatus,
|
||||
handleEditorReady,
|
||||
handleOpenNavigationPane,
|
||||
handlers,
|
||||
@@ -91,7 +89,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
customRealtimeEventHandlers,
|
||||
extendedEditorProps,
|
||||
isFetchingFallbackBinary,
|
||||
hasServerConnectionFailed,
|
||||
onCollaborationStateChange,
|
||||
} = props;
|
||||
|
||||
// states
|
||||
@@ -156,10 +154,14 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
handlers,
|
||||
});
|
||||
|
||||
// Set syncing status on initial render
|
||||
// Set syncing status when page changes and reset collaboration state
|
||||
useEffect(() => {
|
||||
setSyncingStatus("syncing");
|
||||
}, [setSyncingStatus]);
|
||||
onCollaborationStateChange?.({
|
||||
connectionStatus: "connecting",
|
||||
syncStatus: "syncing",
|
||||
});
|
||||
}, [pageId, setSyncingStatus, onCollaborationStateChange]);
|
||||
|
||||
const getAIMenu = useCallback(
|
||||
({ isOpen, onClose }: TAIMenuProps) => (
|
||||
@@ -174,26 +176,24 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
[editorRef, workspaceId, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleServerConnect = useCallback(() => {
|
||||
handleConnectionStatus(false);
|
||||
}, [handleConnectionStatus]);
|
||||
|
||||
const handleServerError = useCallback(() => {
|
||||
handleConnectionStatus(true);
|
||||
setSyncingStatus("error");
|
||||
}, [handleConnectionStatus, setSyncingStatus]);
|
||||
|
||||
const handleServerSynced = useCallback(() => {
|
||||
setSyncingStatus("synced");
|
||||
}, [setSyncingStatus]);
|
||||
|
||||
const serverHandler: TServerHandler = useMemo(
|
||||
() => ({
|
||||
onConnect: handleServerConnect,
|
||||
onServerError: handleServerError,
|
||||
onServerSynced: handleServerSynced,
|
||||
onStateChange: (state) => {
|
||||
// Pass full state to parent
|
||||
onCollaborationStateChange?.(state);
|
||||
|
||||
// Update local syncing status for UI
|
||||
if (state.connectionStatus === "disconnected") {
|
||||
setSyncingStatus("error");
|
||||
} else if (state.connectionStatus === "connected" && state.syncStatus === "synced") {
|
||||
setSyncingStatus("synced");
|
||||
} else {
|
||||
// Handles "connecting", "reconnecting", "connected but not synced", etc.
|
||||
setSyncingStatus("syncing");
|
||||
}
|
||||
},
|
||||
}),
|
||||
[handleServerConnect, handleServerError, handleServerSynced]
|
||||
[setSyncingStatus, onCollaborationStateChange]
|
||||
);
|
||||
|
||||
const realtimeConfig: TRealtimeConfig | undefined = useMemo(() => {
|
||||
@@ -320,7 +320,6 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
}}
|
||||
extendedEditorProps={extendedEditorProps}
|
||||
isFetchingFallbackBinary={isFetchingFallbackBinary}
|
||||
hasServerConnectionFailed={hasServerConnectionFailed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
// plane imports
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import type { CollaborationState, EditorRefApi } from "@plane/editor";
|
||||
import { TOAST_TYPE, updateToast } from "@plane/propel/toast";
|
||||
import type { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types";
|
||||
// hooks
|
||||
@@ -19,10 +19,10 @@ import type { TPageInstance } from "@/store/pages/base-page";
|
||||
import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PageNavigationPaneRoot } from "../navigation-pane";
|
||||
import { PageVersionsOverlay } from "../version";
|
||||
import { PagesVersionEditor } from "../version/editor";
|
||||
import { ContentLimitBanner } from "./content-limit-banner";
|
||||
import { PageEditorBody } from "./editor-body";
|
||||
import type { TEditorBodyConfig, TEditorBodyHandlers } from "./editor-body";
|
||||
import { PageEditorToolbarRoot } from "./toolbar";
|
||||
import { ContentLimitBanner } from "./content-limit-banner";
|
||||
|
||||
export type TPageRootHandlers = {
|
||||
create: (payload: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
|
||||
@@ -57,23 +57,21 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||
workspaceSlug,
|
||||
customRealtimeEventHandlers,
|
||||
} = props;
|
||||
// states
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||
const [collaborationState, setCollaborationState] = useState<CollaborationState | null>(null);
|
||||
const [showContentTooLargeBanner, setShowContentTooLargeBanner] = useState(false);
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// derived values
|
||||
|
||||
const { isNestedPagesEnabled } = usePageStore(storeType);
|
||||
const {
|
||||
isContentEditable,
|
||||
editor: { setEditorRef },
|
||||
} = page;
|
||||
// page fallback
|
||||
|
||||
const { isFetchingFallbackBinary } = usePageFallback({
|
||||
editorRef,
|
||||
fetchPageDescription: handlers.fetchDescriptionBinary,
|
||||
hasConnectionFailed,
|
||||
collaborationState,
|
||||
updatePageDescription: handlers.updateDescription,
|
||||
});
|
||||
|
||||
@@ -156,7 +154,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||
[version, workspaceSlug, page, handlers, editorRef, isNestedPagesEnabled]
|
||||
);
|
||||
|
||||
// reset editor ref on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
setEditorRef(null);
|
||||
@@ -186,7 +183,6 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||
customRealtimeEventHandlers={mergedCustomEventHandlers}
|
||||
editorReady={editorReady}
|
||||
editorForwardRef={editorRef}
|
||||
handleConnectionStatus={setHasConnectionFailed}
|
||||
handleEditorReady={handleEditorReady}
|
||||
handleOpenNavigationPane={handleOpenNavigationPane}
|
||||
handlers={handlers}
|
||||
@@ -198,7 +194,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||
workspaceSlug={workspaceSlug}
|
||||
extendedEditorProps={extendedEditorProps}
|
||||
isFetchingFallbackBinary={isFetchingFallbackBinary}
|
||||
hasServerConnectionFailed={hasConnectionFailed}
|
||||
onCollaborationStateChange={setCollaborationState}
|
||||
/>
|
||||
</div>
|
||||
<PageNavigationPaneRoot
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import type { EditorRefApi, CollaborationState } from "@plane/editor";
|
||||
// plane editor
|
||||
import { getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
|
||||
// plane propel
|
||||
@@ -12,16 +12,19 @@ import useAutoSave from "@/hooks/use-auto-save";
|
||||
type TArgs = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
fetchPageDescription: () => Promise<ArrayBuffer>;
|
||||
hasConnectionFailed: boolean;
|
||||
collaborationState: CollaborationState | null;
|
||||
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
|
||||
};
|
||||
|
||||
export const usePageFallback = (args: TArgs) => {
|
||||
const { editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription } = args;
|
||||
const { editorRef, fetchPageDescription, collaborationState, updatePageDescription } = args;
|
||||
const hasShownFallbackToast = useRef(false);
|
||||
|
||||
const [isFetchingFallbackBinary, setIsFetchingFallbackBinary] = useState(false);
|
||||
|
||||
// Derive connection failure from collaboration state
|
||||
const hasConnectionFailed = collaborationState?.connectionStatus === "disconnected";
|
||||
|
||||
const handleUpdateDescription = useCallback(async () => {
|
||||
if (!hasConnectionFailed) return;
|
||||
const editor = editorRef.current;
|
||||
|
||||
@@ -49,11 +49,16 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
titleRef,
|
||||
updatePageProperties,
|
||||
isFetchingFallbackBinary,
|
||||
hasServerConnectionFailed,
|
||||
} = props;
|
||||
|
||||
// use document editor
|
||||
const { editor, titleEditor, isEditorContentReady, isContentInIndexedDb } = useCollaborativeEditor({
|
||||
const {
|
||||
editor,
|
||||
titleEditor,
|
||||
isEditorContentReady,
|
||||
isContentInIndexedDb,
|
||||
hasServerConnectionFailed: connectionFailed
|
||||
} = useCollaborativeEditor({
|
||||
disabledExtensions,
|
||||
editable,
|
||||
editorClassName,
|
||||
@@ -90,7 +95,7 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
|
||||
if (!editor || !titleEditor) return null;
|
||||
|
||||
const shouldWaitForFallbackBinary = isFetchingFallbackBinary && !isContentInIndexedDb && hasServerConnectionFailed;
|
||||
const shouldWaitForFallbackBinary = isFetchingFallbackBinary && !isContentInIndexedDb && connectionFailed;
|
||||
const isLoading = !isEditorContentReady || shouldWaitForFallbackBinary || pageRestorationInProgress;
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useRealtimeEvents } from "@/hooks/use-realtime-events";
|
||||
import { DocumentEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||
// types
|
||||
import { TCollaborativeEditorHookProps } from "@/types";
|
||||
import type { CollaborationState, ConnectionStatus, SyncStatus } from "@/types/collaboration";
|
||||
// local imports
|
||||
import { useEditorNavigation } from "./use-editor-navigation";
|
||||
import { useTitleEditor } from "./use-title-editor";
|
||||
@@ -62,13 +63,20 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
user,
|
||||
} = props;
|
||||
|
||||
// State machine for collaboration status
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>("initial");
|
||||
const [syncStatus, setSyncStatus] = useState<SyncStatus>("syncing");
|
||||
const [collaborationError, setCollaborationError] = useState<CollaborationState["error"]>();
|
||||
|
||||
// Sync states for loading management
|
||||
const [isContentInIndexedDb, setIsContentInIndexedDb] = useState(false);
|
||||
const [hasServerSynced, setHasServerSynced] = useState(false);
|
||||
const [hasServerConnectionFailed, setHasServerConnectionFailed] = useState(false);
|
||||
const [isIndexedDbSynced, setIsIndexedDbSynced] = useState(false);
|
||||
const [isEditorContentReady, setIsEditorContentReady] = useState(false);
|
||||
|
||||
// Derived values for backward compatibility
|
||||
const hasServerSynced = syncStatus === "synced";
|
||||
const hasServerConnectionFailed = connectionStatus === "disconnected";
|
||||
|
||||
// Connection tracking (ref avoids closure issues in provider callbacks)
|
||||
const connectionRef = useRef({
|
||||
connectionAttempts: 0,
|
||||
@@ -76,51 +84,71 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
forceCloseReceived: false,
|
||||
});
|
||||
|
||||
// Constants
|
||||
const maxConnectionAttempts = 3;
|
||||
|
||||
// Create navigation handlers
|
||||
const { mainNavigationExtension, titleNavigationExtension, setMainEditor, setTitleEditor } = useEditorNavigation();
|
||||
|
||||
// Reset force close state when document ID changes
|
||||
useEffect(() => {
|
||||
setConnectionStatus("initial");
|
||||
setSyncStatus("syncing");
|
||||
setCollaborationError(undefined);
|
||||
setIsContentInIndexedDb(false);
|
||||
setIsIndexedDbSynced(false);
|
||||
setIsEditorContentReady(false);
|
||||
connectionRef.current.connectionAttempts = 0;
|
||||
connectionRef.current.forceCloseReceived = false;
|
||||
connectionRef.current.isPermanentlyStopped = false;
|
||||
}, [id]);
|
||||
|
||||
// Initialize Hocuspocus provider for real-time collaboration
|
||||
useEffect(() => {
|
||||
const state: CollaborationState = {
|
||||
connectionStatus,
|
||||
syncStatus,
|
||||
error: collaborationError,
|
||||
};
|
||||
|
||||
serverHandler.onStateChange(state);
|
||||
}, [connectionStatus, syncStatus, collaborationError, serverHandler]);
|
||||
|
||||
const provider = useMemo(
|
||||
() =>
|
||||
new HocuspocusProvider({
|
||||
name: id,
|
||||
// using user id as a token to verify the user on the server
|
||||
token: JSON.stringify(user),
|
||||
url: realtimeConfig.url,
|
||||
// NOTE: maxAttempts config is broken in Hocuspocus (see GitHub issue #762)
|
||||
// We handle retry limiting manually in the onClose handler below
|
||||
onAuthenticationFailed: () => {
|
||||
serverHandler?.onServerError?.();
|
||||
setHasServerConnectionFailed(true);
|
||||
setConnectionStatus("disconnected");
|
||||
setCollaborationError({ type: "auth-failed", message: "Authentication failed" });
|
||||
},
|
||||
onConnect: () => {
|
||||
// If we've permanently stopped, reject this connection (zombie reconnection from Hocuspocus)
|
||||
if (connectionRef.current.isPermanentlyStopped) {
|
||||
provider?.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset retry counter on successful connection
|
||||
connectionRef.current.connectionAttempts = 0;
|
||||
setConnectionStatus("connected");
|
||||
setCollaborationError(undefined);
|
||||
},
|
||||
onStatus: (status) => {
|
||||
if (status.status === "disconnected") {
|
||||
serverHandler?.onServerError?.();
|
||||
setHasServerConnectionFailed(true);
|
||||
if (status.status === "connecting") {
|
||||
const isReconnecting = connectionRef.current.connectionAttempts > 0;
|
||||
setConnectionStatus(isReconnecting ? "reconnecting" : "connecting");
|
||||
setSyncStatus("syncing"); // Reset sync status during connection attempts
|
||||
} else if (status.status === "disconnected") {
|
||||
setConnectionStatus("disconnected");
|
||||
setSyncStatus("syncing"); // Reset sync status when disconnected
|
||||
setCollaborationError({ type: "network-error", message: "Connection lost" });
|
||||
} else if (status.status === "connected") {
|
||||
setConnectionStatus("connected");
|
||||
setCollaborationError(undefined);
|
||||
}
|
||||
},
|
||||
onSynced: () => {
|
||||
// Reset retry counter on successful sync (stable connection)
|
||||
connectionRef.current.connectionAttempts = 0;
|
||||
serverHandler?.onServerSynced?.();
|
||||
setConnectionStatus("connected"); // Defensive: synced implies connected
|
||||
setSyncStatus("synced");
|
||||
setCollaborationError(undefined);
|
||||
|
||||
const workspaceSlug = new URLSearchParams(realtimeConfig.url).get("workspaceSlug");
|
||||
const projectId = new URLSearchParams(realtimeConfig.url).get("projectId");
|
||||
provider.sendStateless(
|
||||
@@ -130,77 +158,70 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
projectId: projectId,
|
||||
})
|
||||
);
|
||||
setHasServerSynced(true);
|
||||
},
|
||||
}),
|
||||
[id, realtimeConfig.url, serverHandler, user]
|
||||
[id, realtimeConfig.url, user]
|
||||
);
|
||||
|
||||
// Initialize local persistence using IndexedDB
|
||||
const localProvider = useMemo(
|
||||
() => (id ? new IndexeddbPersistence(id, provider.document) : undefined),
|
||||
[id, provider]
|
||||
);
|
||||
|
||||
// Helper function to permanently stop the provider and prevent all reconnection attempts
|
||||
const permanentlyStopProvider = useCallback(() => {
|
||||
const wsProvider = provider.configuration?.websocketProvider;
|
||||
|
||||
// Set flag FIRST to prevent any close event handlers from running
|
||||
connectionRef.current.isPermanentlyStopped = true;
|
||||
|
||||
// Disable reconnection in WebSocket provider
|
||||
if (wsProvider) {
|
||||
wsProvider.shouldConnect = false;
|
||||
}
|
||||
|
||||
// Destroy the provider completely (kills all internal timers and listeners)
|
||||
try {
|
||||
provider.destroy();
|
||||
} catch (error) {
|
||||
console.error(` ✗ Error destroying provider:`, error);
|
||||
console.error(`Error destroying provider:`, error);
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
// Handle connection close events - check close code to determine behavior
|
||||
useEffect(() => {
|
||||
const handleClose = (event: { event?: { code?: number; reason?: string } }) => {
|
||||
const closeCode = event.event?.code;
|
||||
const conn = connectionRef.current;
|
||||
|
||||
// CRITICAL: If we've already permanently stopped, ignore ALL close events
|
||||
if (conn.isPermanentlyStopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Forced close can be detected in two ways:
|
||||
// 1. Close code 4000-4003 (if transmitted correctly by WebSocket)
|
||||
// 2. force_close message received before close event (backup method)
|
||||
const isForcedClose = isForcedCloseCode(closeCode) || conn.forceCloseReceived;
|
||||
|
||||
if (isForcedClose) {
|
||||
// Trigger fallback mechanism (show error UI to user)
|
||||
serverHandler?.onServerError?.();
|
||||
setHasServerConnectionFailed(true);
|
||||
setConnectionStatus("disconnected");
|
||||
setSyncStatus("syncing"); // Reset sync status on forced close
|
||||
setCollaborationError({
|
||||
type: "forced-close",
|
||||
code: closeCode || 0,
|
||||
message: "Server forced connection close",
|
||||
});
|
||||
|
||||
// Permanently stop the provider (destroys all timers)
|
||||
permanentlyStopProvider();
|
||||
|
||||
// Reset values
|
||||
conn.connectionAttempts = 0;
|
||||
conn.forceCloseReceived = false;
|
||||
} else {
|
||||
// Increment connection attempts counter
|
||||
conn.connectionAttempts++;
|
||||
|
||||
// Always trigger fallback
|
||||
serverHandler?.onServerError?.();
|
||||
setHasServerConnectionFailed(true);
|
||||
|
||||
// Stop after max attempts
|
||||
if (conn.connectionAttempts >= maxConnectionAttempts) {
|
||||
// Permanently stop the provider (destroys all timers and prevents zombie reconnections)
|
||||
setConnectionStatus("disconnected");
|
||||
setSyncStatus("syncing"); // Reset sync status after max retries
|
||||
setCollaborationError({
|
||||
type: "max-retries",
|
||||
message: `Failed to connect after ${maxConnectionAttempts} attempts`,
|
||||
});
|
||||
|
||||
permanentlyStopProvider();
|
||||
} else {
|
||||
setConnectionStatus("reconnecting");
|
||||
setSyncStatus("syncing"); // Reset sync status during reconnection
|
||||
setCollaborationError({ type: "network-error", message: "Connection lost, reconnecting..." });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -210,7 +231,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
return () => {
|
||||
provider?.off("close", handleClose);
|
||||
};
|
||||
}, [provider, serverHandler, id, maxConnectionAttempts, permanentlyStopProvider]);
|
||||
}, [provider, maxConnectionAttempts, permanentlyStopProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localProvider) return;
|
||||
@@ -234,7 +255,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
};
|
||||
}, [localProvider, id]);
|
||||
|
||||
// Clear IndexedDB if document is truly empty after both server and IndexedDB have synced
|
||||
useEffect(() => {
|
||||
if (!isIndexedDbSynced || !hasServerSynced || !localProvider) return;
|
||||
const docLength = localProvider.doc.share.get("default")?._length;
|
||||
@@ -245,7 +265,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
}
|
||||
}, [isIndexedDbSynced, hasServerSynced, localProvider, id]);
|
||||
|
||||
// Mark editor content as ready when sync process completes
|
||||
useEffect(() => {
|
||||
if (!isIndexedDbSynced) {
|
||||
return;
|
||||
@@ -262,20 +281,15 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
}
|
||||
}, [isIndexedDbSynced, isContentInIndexedDb, hasServerSynced, hasServerConnectionFailed, id]);
|
||||
|
||||
// Clean up providers on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
// CRITICAL: Set permanently stopped flag BEFORE destroying
|
||||
// This prevents the onClose handler from processing the unmount close event
|
||||
connectionRef.current.isPermanentlyStopped = true;
|
||||
|
||||
provider?.destroy();
|
||||
localProvider?.destroy();
|
||||
},
|
||||
[provider, localProvider]
|
||||
);
|
||||
|
||||
// Initialize main document editor
|
||||
const editor = useEditor({
|
||||
disabledExtensions,
|
||||
extendedEditorProps,
|
||||
@@ -285,13 +299,11 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
editorClassName,
|
||||
enableHistory: false,
|
||||
extensions: [
|
||||
// Core extensions
|
||||
SideMenuExtension({
|
||||
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||
dragDropEnabled,
|
||||
}),
|
||||
HeadingListExtension,
|
||||
// Collaboration extension
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
field: "default",
|
||||
@@ -306,7 +318,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
provider,
|
||||
userDetails: user,
|
||||
}),
|
||||
// Navigation extension for keyboard shortcuts
|
||||
mainNavigationExtension,
|
||||
],
|
||||
fileHandler,
|
||||
@@ -324,12 +335,10 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
// Create setter function for realtime events hook
|
||||
const setForceCloseReceived = useCallback((value: boolean) => {
|
||||
connectionRef.current.forceCloseReceived = value;
|
||||
}, []);
|
||||
|
||||
// Use the new hook for realtime events
|
||||
useRealtimeEvents({
|
||||
editor,
|
||||
provider,
|
||||
@@ -338,7 +347,6 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
setForceCloseReceived,
|
||||
});
|
||||
|
||||
// Initialize title editor
|
||||
const titleEditor = useTitleEditor({
|
||||
id,
|
||||
editable,
|
||||
@@ -346,19 +354,15 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
titleRef,
|
||||
updatePageProperties,
|
||||
extensions: [
|
||||
// Collaboration extension for title field
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
field: "title",
|
||||
}),
|
||||
|
||||
// Navigation extension for keyboard shortcuts
|
||||
titleNavigationExtension,
|
||||
],
|
||||
extendedEditorProps,
|
||||
});
|
||||
|
||||
// Connect editors for navigation once they're initialized
|
||||
useEffect(() => {
|
||||
if (editor && titleEditor) {
|
||||
setMainEditor(editor);
|
||||
@@ -369,6 +373,9 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
return {
|
||||
editor,
|
||||
titleEditor,
|
||||
connectionStatus,
|
||||
syncStatus,
|
||||
collaborationError,
|
||||
hasServerSynced,
|
||||
hasServerConnectionFailed,
|
||||
isContentInIndexedDb,
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
export type ConnectionStatus =
|
||||
| "initial"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "reconnecting"
|
||||
| "disconnected";
|
||||
|
||||
export type SyncStatus =
|
||||
| "syncing"
|
||||
| "synced";
|
||||
|
||||
export type CollaborationError =
|
||||
| { type: "auth-failed"; message: string }
|
||||
| { type: "network-error"; message: string }
|
||||
| { type: "forced-close"; code: number; message: string }
|
||||
| { type: "max-retries"; message: string };
|
||||
|
||||
export type CollaborationState = {
|
||||
connectionStatus: ConnectionStatus;
|
||||
syncStatus: SyncStatus;
|
||||
error?: CollaborationError;
|
||||
};
|
||||
|
||||
export type TServerHandler = {
|
||||
onConnect?: () => void;
|
||||
onServerError?: () => void;
|
||||
onServerSynced?: () => void;
|
||||
onStateChange: (state: CollaborationState) => void;
|
||||
};
|
||||
|
||||
@@ -198,7 +198,7 @@ export type ICollaborativeDocumentEditorProps = Omit<IEditorProps, "initialValue
|
||||
dragDropEnabled?: boolean;
|
||||
editable: boolean;
|
||||
realtimeConfig: TRealtimeConfig;
|
||||
serverHandler?: TServerHandler;
|
||||
serverHandler: TServerHandler;
|
||||
user: TUserDetails;
|
||||
updatePageProperties?: <T extends keyof EventToPayloadMap>(
|
||||
pageIds: string | string[],
|
||||
@@ -210,7 +210,6 @@ export type ICollaborativeDocumentEditorProps = Omit<IEditorProps, "initialValue
|
||||
titleRef?: React.MutableRefObject<EditorTitleRefApi | null>;
|
||||
extendedDocumentEditorProps?: ICollaborativeDocumentEditorPropsExtended;
|
||||
isFetchingFallbackBinary?: boolean;
|
||||
hasServerConnectionFailed?: boolean;
|
||||
};
|
||||
|
||||
export type IDocumentEditorProps = Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> & {
|
||||
|
||||
Reference in New Issue
Block a user