-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[WIKI-844] fix: realtime sync post vite migration with title editor sync and indexed db access #8294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
[WIKI-844] fix: realtime sync post vite migration with title editor sync and indexed db access #8294
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
83065ca
fix: robust way to handle socket connection and read from indexeddb c…
Palanikannan1437 e4f7395
fix: realtime sync working with failure handling
Palanikannan1437 ead04c8
fix: title editor added
Palanikannan1437 b4fb4b6
merge preview into fix/realtime-sync
Palanikannan1437 60d4fcb
check
Palanikannan1437 625124d
check
Palanikannan1437 c8fa092
page renderer props
Palanikannan1437 d9aeae2
lint errors
Palanikannan1437 ebd097d
lint errors
Palanikannan1437 35f0e64
lint errors
Palanikannan1437 43bf244
sanitize html
Palanikannan1437 92aa3f7
sanitize html
Palanikannan1437 253297f
format fix
Palanikannan1437 c2a4617
fix lint
Palanikannan1437 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| // hocuspocus | ||
| import type { Extension, Hocuspocus, Document } from "@hocuspocus/server"; | ||
| import { TiptapTransformer } from "@hocuspocus/transformer"; | ||
| import type * as Y from "yjs"; | ||
| // editor extensions | ||
| import { TITLE_EDITOR_EXTENSIONS, createRealtimeEvent } from "@plane/editor"; | ||
| import { logger } from "@plane/logger"; | ||
| import { AppError } from "@/lib/errors"; | ||
| // helpers | ||
| import { getPageService } from "@/services/page/handler"; | ||
| import type { HocusPocusServerContext, OnLoadDocumentPayloadWithContext } from "@/types"; | ||
| import { generateTitleProsemirrorJson } from "@/utils"; | ||
| import { broadcastMessageToPage } from "@/utils/broadcast-message"; | ||
| import { TitleUpdateManager } from "./title-update/title-update-manager"; | ||
| import { extractTextFromHTML } from "./title-update/title-utils"; | ||
|
|
||
| /** | ||
| * Hocuspocus extension for synchronizing document titles | ||
| */ | ||
| export class TitleSyncExtension implements Extension { | ||
| // Maps document names to their observers and update managers | ||
| private titleObservers: Map<string, (events: Y.YEvent<any>[]) => void> = new Map(); | ||
| private titleUpdateManagers: Map<string, TitleUpdateManager> = new Map(); | ||
| // Store minimal data needed for each document's title observer (prevents closure memory leaks) | ||
| private titleObserverData: Map< | ||
| string, | ||
| { | ||
| parentId?: string | null; | ||
| userId: string; | ||
| workspaceSlug: string | null; | ||
| instance: Hocuspocus; | ||
| } | ||
| > = new Map(); | ||
|
|
||
| /** | ||
| * Handle document loading - migrate old titles if needed | ||
| */ | ||
| async onLoadDocument({ context, document, documentName }: OnLoadDocumentPayloadWithContext) { | ||
| try { | ||
| // initially for on demand migration of old titles to a new title field | ||
| // in the yjs binary | ||
| if (document.isEmpty("title")) { | ||
| const service = getPageService(context.documentType, context); | ||
| // const title = await service.fe | ||
| const title = (await service.fetchDetails?.(documentName)).name; | ||
| if (title == null) return; | ||
sriramveeraghanta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const titleField = TiptapTransformer.toYdoc( | ||
| generateTitleProsemirrorJson(title), | ||
| "title", | ||
| // editor | ||
| TITLE_EDITOR_EXTENSIONS as any | ||
| ); | ||
| document.merge(titleField); | ||
| } | ||
| } catch (error) { | ||
| const appError = new AppError(error, { | ||
| context: { operation: "onLoadDocument", documentName }, | ||
| }); | ||
| logger.error("Error loading document title", appError); | ||
| } | ||
| } | ||
| /** | ||
| * Set up title synchronization for a document after it's loaded | ||
| */ | ||
| async afterLoadDocument({ | ||
| document, | ||
| documentName, | ||
| context, | ||
| instance, | ||
| }: { | ||
| document: Document; | ||
| documentName: string; | ||
| context: HocusPocusServerContext; | ||
| instance: Hocuspocus; | ||
| }) { | ||
| // Create a title update manager for this document | ||
| const updateManager = new TitleUpdateManager(documentName, context); | ||
|
|
||
| // Store the manager | ||
| this.titleUpdateManagers.set(documentName, updateManager); | ||
|
|
||
| // Store minimal data needed for the observer (prevents closure memory leak) | ||
| this.titleObserverData.set(documentName, { | ||
| userId: context.userId, | ||
| workspaceSlug: context.workspaceSlug, | ||
| instance: instance, | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Create observer using bound method to avoid closure capturing heavy objects | ||
| const titleObserver = this.handleTitleChange.bind(this, documentName); | ||
|
|
||
| // Observe the title field | ||
| document.getXmlFragment("title").observeDeep(titleObserver); | ||
| this.titleObservers.set(documentName, titleObserver); | ||
| } | ||
|
|
||
| /** | ||
| * Handle title changes for a document | ||
| * This is a separate method to avoid closure memory leaks | ||
| */ | ||
| private handleTitleChange(documentName: string, events: Y.YEvent<any>[]) { | ||
| let title = ""; | ||
| events.forEach((event) => { | ||
| title = extractTextFromHTML(event.currentTarget.toJSON() as string); | ||
| }); | ||
|
|
||
| // Get the manager for this document | ||
| const manager = this.titleUpdateManagers.get(documentName); | ||
|
|
||
| // Get the stored data for this document | ||
| const data = this.titleObserverData.get(documentName); | ||
|
|
||
| // Broadcast to parent page if it exists | ||
| if (data?.parentId && data.workspaceSlug && data.instance) { | ||
| const event = createRealtimeEvent({ | ||
| user_id: data.userId, | ||
| workspace_slug: data.workspaceSlug, | ||
| action: "property_updated", | ||
| page_id: documentName, | ||
| data: { name: title }, | ||
| descendants_ids: [], | ||
| }); | ||
|
|
||
| // Use the instance from stored data (guaranteed to be set) | ||
| broadcastMessageToPage(data.instance, data.parentId, event); | ||
| } | ||
|
|
||
| // Schedule the title update | ||
| if (manager) { | ||
| manager.scheduleUpdate(title); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Force save title before unloading the document | ||
| */ | ||
| async beforeUnloadDocument({ documentName }: { documentName: string }) { | ||
| const updateManager = this.titleUpdateManagers.get(documentName); | ||
| if (updateManager) { | ||
| // Force immediate save and wait for it to complete | ||
| await updateManager.forceSave(); | ||
| // Clean up the manager | ||
| this.titleUpdateManagers.delete(documentName); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Remove observers after document unload | ||
| */ | ||
| async afterUnloadDocument({ documentName, document }: { documentName: string; document?: Document }) { | ||
| // Clean up observer when document is unloaded | ||
| const observer = this.titleObservers.get(documentName); | ||
| if (observer) { | ||
| // unregister observer from Y.js document to prevent memory leak | ||
| if (document) { | ||
| try { | ||
| document.getXmlFragment("title").unobserveDeep(observer); | ||
| } catch (error) { | ||
| logger.error("Failed to unobserve title field", new AppError(error, { context: { documentName } })); | ||
| } | ||
| } | ||
| this.titleObservers.delete(documentName); | ||
| } | ||
|
|
||
| // Clean up the observer data map to prevent memory leak | ||
| this.titleObserverData.delete(documentName); | ||
|
|
||
| // Ensure manager is cleaned up if beforeUnloadDocument somehow didn't run | ||
| if (this.titleUpdateManagers.has(documentName)) { | ||
| const manager = this.titleUpdateManagers.get(documentName)!; | ||
| manager.cancel(); | ||
| this.titleUpdateManagers.delete(documentName); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.