mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 20:20:49 +01:00
fix: title sync done
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PageService } from "@/core/services/page.service";
|
||||
import { transformHTMLToBinary } from "./transformers";
|
||||
import { getAllDocumentFormatsFromBinaryData } from "@/core/helpers/page";
|
||||
import { logger } from "@plane/logger";
|
||||
import { HocusPocusServerContext } from "@/core/types/common";
|
||||
|
||||
const pageService = new PageService();
|
||||
|
||||
@@ -9,13 +10,15 @@ const pageService = new PageService();
|
||||
* Fetches the binary description data for a project page
|
||||
* Falls back to HTML transformation if binary is not available
|
||||
*/
|
||||
export const fetchPageDescriptionBinary = async (
|
||||
params: URLSearchParams,
|
||||
pageId: string,
|
||||
cookie: string | undefined
|
||||
) => {
|
||||
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
||||
const projectId = params.get("projectId")?.toString();
|
||||
export const fetchPageDescriptionBinary = async ({
|
||||
pageId,
|
||||
context,
|
||||
}: {
|
||||
pageId: string;
|
||||
context: HocusPocusServerContext;
|
||||
}) => {
|
||||
const { workspaceSlug, projectId, cookie } = context;
|
||||
|
||||
if (!workspaceSlug || !projectId || !cookie) return null;
|
||||
|
||||
const response = await pageService.fetchDescriptionBinary(workspaceSlug, projectId, pageId, cookie);
|
||||
@@ -35,24 +38,21 @@ export const fetchPageDescriptionBinary = async (
|
||||
* Updates the description of a project page
|
||||
*/
|
||||
export const updatePageDescription = async ({
|
||||
params,
|
||||
context,
|
||||
pageId,
|
||||
updatedDescription,
|
||||
cookie,
|
||||
state: updatedDescription,
|
||||
title,
|
||||
}: {
|
||||
params: URLSearchParams | undefined;
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
updatedDescription: Uint8Array;
|
||||
cookie: string | undefined;
|
||||
state: Uint8Array;
|
||||
title: string;
|
||||
}) => {
|
||||
if (!(updatedDescription instanceof Uint8Array)) {
|
||||
throw new Error("Invalid updatedDescription: must be an instance of Uint8Array");
|
||||
}
|
||||
|
||||
const workspaceSlug = params?.get("workspaceSlug")?.toString();
|
||||
const projectId = params?.get("projectId")?.toString();
|
||||
const { workspaceSlug, projectId, cookie } = context;
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData(updatedDescription);
|
||||
@@ -67,17 +67,14 @@ export const updatePageDescription = async ({
|
||||
};
|
||||
|
||||
export const fetchProjectPageTitle = async ({
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
context,
|
||||
pageId,
|
||||
cookie,
|
||||
}: {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
cookie: string | undefined;
|
||||
}) => {
|
||||
if (!workspaceSlug || !cookie) return;
|
||||
const { workspaceSlug, projectId, cookie } = context;
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
try {
|
||||
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
|
||||
@@ -89,20 +86,17 @@ export const fetchProjectPageTitle = async ({
|
||||
};
|
||||
|
||||
export const updateProjectPageTitle = async ({
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
context,
|
||||
pageId,
|
||||
title,
|
||||
cookie,
|
||||
abortSignal,
|
||||
}: {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
title: string;
|
||||
cookie: string | undefined;
|
||||
abortSignal?: AbortSignal;
|
||||
}) => {
|
||||
const { workspaceSlug, projectId, cookie } = context;
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const payload = {
|
||||
|
||||
@@ -19,32 +19,19 @@ export const projectPageHandler: DocumentHandler = {
|
||||
/**
|
||||
* Fetch project page description
|
||||
*/
|
||||
fetch: async ({ pageId, params, context }: DocumentFetchParams) => {
|
||||
const { cookie } = context;
|
||||
return await fetchPageDescriptionBinary(params, pageId, cookie);
|
||||
},
|
||||
|
||||
fetch: fetchPageDescriptionBinary,
|
||||
/**
|
||||
* Store project page description
|
||||
*/
|
||||
store: async ({ pageId, state, params, context, title }: DocumentStoreParams) => {
|
||||
const { cookie } = context;
|
||||
await updatePageDescription({ params, pageId, updatedDescription: state, cookie, title });
|
||||
},
|
||||
|
||||
store: updatePageDescription,
|
||||
/**
|
||||
* Fetch project page title
|
||||
*/
|
||||
fetchTitle: async ({ workspaceSlug, projectId, pageId, cookie }) => {
|
||||
return await fetchProjectPageTitle({ workspaceSlug, projectId, pageId, cookie });
|
||||
},
|
||||
|
||||
fetchTitle: fetchProjectPageTitle,
|
||||
/**
|
||||
* Store project page title
|
||||
*/
|
||||
updateTitle: async ({ workspaceSlug, projectId, pageId, title, cookie, abortSignal }) => {
|
||||
await updateProjectPageTitle({ workspaceSlug, projectId, pageId, title, cookie, abortSignal });
|
||||
},
|
||||
updateTitle: updateProjectPageTitle,
|
||||
};
|
||||
|
||||
// Define the project page handler definition
|
||||
|
||||
@@ -16,15 +16,11 @@ export const createDatabaseExtension = () => {
|
||||
const handleFetch = async ({
|
||||
context,
|
||||
documentName: pageId,
|
||||
requestParameters,
|
||||
}: {
|
||||
context: HocusPocusServerContext;
|
||||
documentName: TDocumentTypes;
|
||||
requestParameters: URLSearchParams;
|
||||
}) => {
|
||||
const { documentType } = context;
|
||||
const params = requestParameters;
|
||||
|
||||
let fetchedData = null;
|
||||
fetchedData = await catchAsync(
|
||||
async () => {
|
||||
@@ -39,11 +35,10 @@ const handleFetch = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const documentHandler = getDocumentHandler(documentType);
|
||||
const documentHandler = getDocumentHandler(context);
|
||||
fetchedData = await documentHandler.fetch({
|
||||
context: context as HocusPocusServerContext,
|
||||
pageId,
|
||||
params,
|
||||
});
|
||||
|
||||
if (!fetchedData) {
|
||||
@@ -70,7 +65,6 @@ const handleStore = async ({
|
||||
context,
|
||||
state,
|
||||
documentName: pageId,
|
||||
requestParameters,
|
||||
document,
|
||||
}: Partial<storePayload> & {
|
||||
context: HocusPocusServerContext;
|
||||
@@ -93,7 +87,6 @@ const handleStore = async ({
|
||||
title = extractTextFromHTML(document?.getXmlFragment("title")?.toJSON());
|
||||
}
|
||||
const { documentType } = context as HocusPocusServerContext;
|
||||
const params = requestParameters;
|
||||
if (!documentType) {
|
||||
handleError(null, {
|
||||
errorType: "bad-request",
|
||||
@@ -105,12 +98,11 @@ const handleStore = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const documentHandler = getDocumentHandler(documentType);
|
||||
const documentHandler = getDocumentHandler(context);
|
||||
await documentHandler.store({
|
||||
context: context as HocusPocusServerContext,
|
||||
pageId,
|
||||
state,
|
||||
params,
|
||||
title,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Logger } from "@hocuspocus/extension-logger";
|
||||
import { setupRedisExtension } from "@/core/extensions/redis";
|
||||
import { createDatabaseExtension } from "@/core/extensions/database";
|
||||
import { logger } from "@plane/logger";
|
||||
import { TitleSyncExtension } from "./title-sync";
|
||||
|
||||
export const getExtensions = async (): Promise<Extension[]> => {
|
||||
const extensions: Extension[] = [
|
||||
@@ -16,6 +17,9 @@ export const getExtensions = async (): Promise<Extension[]> => {
|
||||
createDatabaseExtension(),
|
||||
];
|
||||
|
||||
const titleSyncExtension = new TitleSyncExtension();
|
||||
extensions.push(titleSyncExtension);
|
||||
|
||||
// Add Redis extensions if Redis is available
|
||||
const redisExtensions = await setupRedisExtension();
|
||||
extensions.push(...redisExtensions);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// hocuspocus
|
||||
import { Extension, afterLoadDocumentPayload, Hocuspocus, Document } from "@hocuspocus/server";
|
||||
import { Extension, Hocuspocus, Document } from "@hocuspocus/server";
|
||||
import { TiptapTransformer } from "@hocuspocus/transformer";
|
||||
import * as Y from "yjs";
|
||||
// types
|
||||
@@ -18,38 +18,22 @@ import { TitleUpdateManager } from "./title-update/title-update-manager";
|
||||
*/
|
||||
export class TitleSyncExtension implements Extension {
|
||||
instance!: Hocuspocus;
|
||||
|
||||
|
||||
// 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();
|
||||
|
||||
/**
|
||||
* Handle document loading - migrate old titles if needed
|
||||
*/
|
||||
async onLoadDocument({
|
||||
context,
|
||||
document,
|
||||
requestParameters,
|
||||
}: {
|
||||
context: HocusPocusServerContext;
|
||||
document: Document;
|
||||
requestParameters: URLSearchParams;
|
||||
}) {
|
||||
|
||||
async onLoadDocument({ context, document }: { context: HocusPocusServerContext; document: Document }) {
|
||||
try {
|
||||
// initially for on demand migration of old titles to a new title field
|
||||
// in the yjs binary
|
||||
if (document.isEmpty("title")) {
|
||||
const typedContext = context as HocusPocusServerContext;
|
||||
const workspaceSlug = requestParameters.get("workspaceSlug")?.toString();
|
||||
const projectId = requestParameters.get("projectId")?.toString();
|
||||
|
||||
const documentHandler = getDocumentHandler(typedContext.documentType);
|
||||
const { workspaceSlug, projectId } = context;
|
||||
const documentHandler = getDocumentHandler(context);
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const title = await documentHandler.fetchTitle({
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
context,
|
||||
pageId: document.name,
|
||||
cookie: typedContext.cookie,
|
||||
});
|
||||
if (title == null) return;
|
||||
const titleField = TiptapTransformer.toYdoc(
|
||||
@@ -66,46 +50,47 @@ export class TitleSyncExtension implements Extension {
|
||||
/**
|
||||
* Set up title synchronization for a document after it's loaded
|
||||
*/
|
||||
async afterLoadDocument({ document, documentName, context, requestParameters }: afterLoadDocumentPayload) {
|
||||
const workspaceSlug = requestParameters.get("workspaceSlug")?.toString();
|
||||
const projectId = requestParameters.get("projectId")?.toString();
|
||||
|
||||
async afterLoadDocument({
|
||||
document,
|
||||
documentName,
|
||||
context,
|
||||
}: {
|
||||
document: Document;
|
||||
documentName: string;
|
||||
context: HocusPocusServerContext;
|
||||
}) {
|
||||
const { workspaceSlug, projectId } = context;
|
||||
|
||||
// Exit if we don't have the required information
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const documentHandler = getDocumentHandler(context.documentType);
|
||||
|
||||
|
||||
const documentHandler = getDocumentHandler(context);
|
||||
|
||||
// Create a title update manager for this document
|
||||
const updateManager = new TitleUpdateManager(
|
||||
documentName,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
context.cookie,
|
||||
documentHandler
|
||||
);
|
||||
|
||||
const updateManager = new TitleUpdateManager(documentName, documentHandler, context);
|
||||
|
||||
// Store the manager
|
||||
this.titleUpdateManagers.set(documentName, updateManager);
|
||||
|
||||
|
||||
// Set up observer for title field
|
||||
const titleObserver = (events: Y.YEvent<any>[]) => {
|
||||
let title = "";
|
||||
events.forEach((event) => {
|
||||
title = extractTextFromHTML(event.currentTarget.toJSON());
|
||||
});
|
||||
|
||||
|
||||
// Schedule an update with the manager
|
||||
const manager = this.titleUpdateManagers.get(documentName);
|
||||
if (manager) {
|
||||
manager.scheduleUpdate(title);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Observe the title field
|
||||
document.getXmlFragment("title").observeDeep(titleObserver);
|
||||
this.titleObservers.set(documentName, titleObserver);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force save title before unloading the document
|
||||
*/
|
||||
@@ -118,7 +103,7 @@ export class TitleSyncExtension implements Extension {
|
||||
this.titleUpdateManagers.delete(documentName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove observers after document unload
|
||||
*/
|
||||
@@ -126,10 +111,9 @@ export class TitleSyncExtension implements Extension {
|
||||
// Clean up observer when document is unloaded
|
||||
const observer = this.titleObservers.get(documentName);
|
||||
if (observer) {
|
||||
console.log(`Removing title observer for ${documentName}`);
|
||||
this.titleObservers.delete(documentName);
|
||||
}
|
||||
|
||||
|
||||
// Ensure manager is cleaned up if beforeUnloadDocument somehow didn't run
|
||||
if (this.titleUpdateManagers.has(documentName)) {
|
||||
const manager = this.titleUpdateManagers.get(documentName)!;
|
||||
|
||||
@@ -28,7 +28,7 @@ export const createDebounceState = (): DebounceState => ({
|
||||
export interface DebounceOptions {
|
||||
/** The wait time in milliseconds */
|
||||
wait: number;
|
||||
|
||||
|
||||
/** Optional logging prefix for debug messages */
|
||||
logPrefix?: string;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class DebounceManager {
|
||||
private state: DebounceState;
|
||||
private wait: number;
|
||||
private logPrefix: string;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new DebounceManager
|
||||
* @param options Debounce configuration options
|
||||
@@ -49,9 +49,9 @@ export class DebounceManager {
|
||||
constructor(options: DebounceOptions) {
|
||||
this.state = createDebounceState();
|
||||
this.wait = options.wait;
|
||||
this.logPrefix = options.logPrefix || '';
|
||||
this.logPrefix = options.logPrefix || "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Schedule a debounced function call
|
||||
* @param func The function to call
|
||||
@@ -60,56 +60,56 @@ export class DebounceManager {
|
||||
schedule(func: (...args: any[]) => Promise<void>, ...args: any[]): void {
|
||||
// Always update the last arguments
|
||||
this.state.lastArgs = args;
|
||||
|
||||
|
||||
const time = Date.now();
|
||||
this.state.lastCallTime = time;
|
||||
|
||||
|
||||
// If an operation is in progress, just store the new args and start the timer
|
||||
if (this.state.inProgress) {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Operation in progress, storing new args and starting new timer`);
|
||||
}
|
||||
|
||||
|
||||
// Always restart the timer for the new call, even if an operation is in progress
|
||||
if (this.state.timerId) {
|
||||
clearTimeout(this.state.timerId);
|
||||
}
|
||||
|
||||
|
||||
this.state.timerId = setTimeout(() => {
|
||||
this.timerExpired(func);
|
||||
}, this.wait);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// If already scheduled, update the args and restart the timer
|
||||
if (this.state.timerId) {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Already scheduled, updating args and restarting timer`);
|
||||
}
|
||||
|
||||
|
||||
clearTimeout(this.state.timerId);
|
||||
this.state.timerId = setTimeout(() => {
|
||||
this.timerExpired(func);
|
||||
}, this.wait);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Start the timer for the trailing edge execution
|
||||
this.state.timerId = setTimeout(() => {
|
||||
this.timerExpired(func);
|
||||
}, this.wait);
|
||||
|
||||
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Scheduled execution with wait time ${this.wait}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when the timer expires
|
||||
*/
|
||||
private timerExpired(func: (...args: any[]) => Promise<void>): void {
|
||||
const time = Date.now();
|
||||
|
||||
|
||||
// Check if this timer expiration represents the end of the debounce period
|
||||
if (this.shouldInvoke(time)) {
|
||||
// If an operation is already in progress, abort it if the debounce period has completed
|
||||
@@ -118,104 +118,104 @@ export class DebounceManager {
|
||||
console.log(`${this.logPrefix}: Timer expired while operation in progress - will abort current operation`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Execute the function
|
||||
this.executeFunction(func, time);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise restart the timer
|
||||
this.state.timerId = setTimeout(() => {
|
||||
this.timerExpired(func);
|
||||
}, this.remainingWait(time));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the debounced function
|
||||
*/
|
||||
private executeFunction(func: (...args: any[]) => Promise<void>, time: number): void {
|
||||
this.state.timerId = null;
|
||||
this.state.lastExecutionTime = time;
|
||||
|
||||
|
||||
// Execute the function asynchronously
|
||||
this.performFunction(func).catch(error => {
|
||||
this.performFunction(func).catch((error) => {
|
||||
console.error(`${this.logPrefix}: Error in execution:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Perform the actual function call, handling any in-progress operations
|
||||
*/
|
||||
private async performFunction(func: (...args: any[]) => Promise<void>): Promise<void> {
|
||||
const args = this.state.lastArgs;
|
||||
if (!args) return;
|
||||
|
||||
|
||||
// Store the args we're about to use
|
||||
const currentArgs = [...args];
|
||||
|
||||
|
||||
// If another operation is in progress, abort it
|
||||
await this.abortOngoingOperation();
|
||||
|
||||
|
||||
// Mark that we're starting a new operation
|
||||
this.state.inProgress = true;
|
||||
this.state.abortController = new AbortController();
|
||||
|
||||
|
||||
try {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Starting operation`);
|
||||
}
|
||||
|
||||
|
||||
// Add the abort signal to the arguments if the function can use it
|
||||
const execArgs = [...currentArgs];
|
||||
execArgs.push(this.state.abortController.signal);
|
||||
|
||||
|
||||
await func(...execArgs);
|
||||
|
||||
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Completed operation`);
|
||||
}
|
||||
|
||||
|
||||
// Only clear lastArgs if they haven't been changed during this operation
|
||||
if (this.state.lastArgs && this.arraysEqual(this.state.lastArgs, currentArgs)) {
|
||||
this.state.lastArgs = null;
|
||||
|
||||
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Args have not changed during operation, clearing lastArgs`);
|
||||
}
|
||||
|
||||
|
||||
// Clear any timer as we've successfully processed the latest args
|
||||
if (this.state.timerId) {
|
||||
clearTimeout(this.state.timerId);
|
||||
this.state.timerId = null;
|
||||
}
|
||||
} else if (this.state.lastArgs) {
|
||||
// If lastArgs have changed during this operation, the timer should already be running
|
||||
// If lastArgs have changed during this operation, the timer should already be running
|
||||
// but let's make sure it is
|
||||
if (!this.state.timerId) {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Args changed during operation, ensuring timer is running`);
|
||||
}
|
||||
|
||||
|
||||
this.state.timerId = setTimeout(() => {
|
||||
this.timerExpired(func);
|
||||
}, this.wait);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Operation was aborted, another operation should be starting`);
|
||||
}
|
||||
// Nothing to do here, the new operation will be triggered by the timer expiration
|
||||
} else {
|
||||
console.error(`${this.logPrefix}: Error during operation:`, error);
|
||||
|
||||
|
||||
// On error (not abort), make sure we have a timer running to retry
|
||||
if (!this.state.timerId && this.state.lastArgs) {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Rescheduling failed operation`);
|
||||
}
|
||||
|
||||
|
||||
this.state.timerId = setTimeout(() => {
|
||||
this.timerExpired(func);
|
||||
}, this.wait);
|
||||
@@ -226,7 +226,7 @@ export class DebounceManager {
|
||||
this.state.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abort any ongoing operation
|
||||
*/
|
||||
@@ -235,35 +235,32 @@ export class DebounceManager {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Aborting in-progress operation`);
|
||||
}
|
||||
|
||||
|
||||
this.state.abortController.abort();
|
||||
|
||||
|
||||
// Small delay to ensure the abort has had time to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
// Double-check that state has been reset, force it if not
|
||||
if (this.state.inProgress || this.state.abortController) {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Force resetting in-progress state after abort`);
|
||||
}
|
||||
|
||||
|
||||
this.state.inProgress = false;
|
||||
this.state.abortController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if we should invoke the function now
|
||||
*/
|
||||
private shouldInvoke(time: number): boolean {
|
||||
// Either this is the first call, or we've waited long enough since the last call
|
||||
return (
|
||||
this.state.lastCallTime === undefined ||
|
||||
(time - this.state.lastCallTime) >= this.wait
|
||||
);
|
||||
return this.state.lastCallTime === undefined || time - this.state.lastCallTime >= this.wait;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate how much longer we should wait
|
||||
*/
|
||||
@@ -271,7 +268,7 @@ export class DebounceManager {
|
||||
const timeSinceLastCall = time - (this.state.lastCallTime || 0);
|
||||
return Math.max(0, this.wait - timeSinceLastCall);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force immediate execution
|
||||
*/
|
||||
@@ -279,16 +276,16 @@ export class DebounceManager {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Force immediate execution`);
|
||||
}
|
||||
|
||||
|
||||
// Clear any pending timeout
|
||||
if (this.state.timerId) {
|
||||
clearTimeout(this.state.timerId);
|
||||
this.state.timerId = null;
|
||||
}
|
||||
|
||||
|
||||
// Reset timing state
|
||||
this.state.lastCallTime = undefined;
|
||||
|
||||
|
||||
// Perform the function immediately
|
||||
if (this.state.lastArgs) {
|
||||
await this.performFunction(func);
|
||||
@@ -302,7 +299,7 @@ export class DebounceManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancel any pending operations without executing
|
||||
*/
|
||||
@@ -310,27 +307,27 @@ export class DebounceManager {
|
||||
if (this.logPrefix) {
|
||||
console.log(`${this.logPrefix}: Cancelling pending operations`);
|
||||
}
|
||||
|
||||
|
||||
// Clear any pending timeout
|
||||
if (this.state.timerId) {
|
||||
clearTimeout(this.state.timerId);
|
||||
this.state.timerId = null;
|
||||
}
|
||||
|
||||
|
||||
// Reset timing state
|
||||
this.state.lastCallTime = undefined;
|
||||
|
||||
|
||||
// Abort any in-progress operation
|
||||
if (this.state.inProgress && this.state.abortController) {
|
||||
this.state.abortController.abort();
|
||||
this.state.inProgress = false;
|
||||
this.state.abortController = null;
|
||||
}
|
||||
|
||||
|
||||
// Clear args
|
||||
this.state.lastArgs = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two arrays for equality
|
||||
*/
|
||||
@@ -341,4 +338,5 @@ export class DebounceManager {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DocumentHandler } from "@/core/types/document-handler";
|
||||
import { DebounceManager } from "./debounce";
|
||||
import { HocusPocusServerContext } from "@/core/types/common";
|
||||
|
||||
/**
|
||||
* Manages title update operations for a single document
|
||||
@@ -7,28 +8,22 @@ import { DebounceManager } from "./debounce";
|
||||
*/
|
||||
export class TitleUpdateManager {
|
||||
private documentName: string;
|
||||
private workspaceSlug: string;
|
||||
private projectId: string;
|
||||
private cookie: string;
|
||||
private documentHandler: DocumentHandler;
|
||||
private debounceManager: DebounceManager;
|
||||
private lastTitle: string | null = null;
|
||||
private context: HocusPocusServerContext;
|
||||
|
||||
/**
|
||||
* Create a new TitleUpdateManager instance
|
||||
*/
|
||||
constructor(
|
||||
documentName: string,
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cookie: string,
|
||||
documentHandler: any,
|
||||
documentHandler: DocumentHandler,
|
||||
context: HocusPocusServerContext,
|
||||
wait: number = 5000
|
||||
) {
|
||||
this.context = context;
|
||||
this.documentName = documentName;
|
||||
this.workspaceSlug = workspaceSlug;
|
||||
this.projectId = projectId;
|
||||
this.cookie = cookie;
|
||||
this.documentHandler = documentHandler;
|
||||
|
||||
// Set up debounce manager with logging
|
||||
@@ -61,14 +56,12 @@ export class TitleUpdateManager {
|
||||
try {
|
||||
console.log(`Starting title update for ${this.documentName} with: "${title}"`);
|
||||
|
||||
await this.documentHandler.updateTitle(
|
||||
this.workspaceSlug,
|
||||
this.projectId,
|
||||
this.documentName,
|
||||
await this.documentHandler.updateTitle({
|
||||
context: this.context,
|
||||
pageId: this.documentName,
|
||||
title,
|
||||
this.cookie,
|
||||
signal
|
||||
);
|
||||
abortSignal: signal,
|
||||
});
|
||||
|
||||
console.log(`Completed title update for ${this.documentName} with: "${title}"`);
|
||||
|
||||
|
||||
@@ -13,17 +13,7 @@ initializeDocumentHandlers();
|
||||
* @param additionalContext Optional additional context criteria
|
||||
* @returns The appropriate document handler
|
||||
*/
|
||||
export function getDocumentHandler(
|
||||
documentType: string,
|
||||
additionalContext: Omit<HocusPocusServerContext, "documentType">
|
||||
): DocumentHandler {
|
||||
// Create a context object with all criteria
|
||||
const context: HocusPocusServerContext = {
|
||||
documentType: documentType as any,
|
||||
...additionalContext,
|
||||
};
|
||||
|
||||
// Use the factory to get the appropriate handler
|
||||
export function getDocumentHandler(context: HocusPocusServerContext): DocumentHandler {
|
||||
return handlerFactory.getHandler(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { handleAuthentication } from "@/core/lib/authentication";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
import { TitleSyncExtension } from "./extensions/title-sync";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
// types
|
||||
@@ -70,6 +69,7 @@ export const getHocusPocusServer = async () => {
|
||||
context.cookie = cookie ?? requestParameters.get("cookie");
|
||||
context.userId = userId;
|
||||
context.workspaceSlug = requestParameters.get("workspaceSlug")?.toString() as string;
|
||||
context.projectId = requestParameters.get("projectId")?.toString() as string;
|
||||
|
||||
return await handleAuthentication({
|
||||
cookie: context.cookie,
|
||||
@@ -95,7 +95,7 @@ export const getHocusPocusServer = async () => {
|
||||
{ extra: { operation: "stateless", payload } }
|
||||
);
|
||||
},
|
||||
extensions: [...extensions, new TitleSyncExtension()],
|
||||
debounce: 10000,
|
||||
extensions,
|
||||
debounce: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,11 +44,8 @@ export class PageService extends APIService {
|
||||
cookie: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<any> {
|
||||
console.log("aaya update call", data);
|
||||
|
||||
// Early abort check
|
||||
if (abortSignal?.aborted) {
|
||||
console.log(`Title update was already aborted before starting: ${pageId}`);
|
||||
throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
|
||||
@@ -57,7 +54,6 @@ export class PageService extends APIService {
|
||||
const abortPromise = new Promise((_, reject) => {
|
||||
if (abortSignal) {
|
||||
abortListener = () => {
|
||||
console.log(`Title update aborted during execution: ${pageId}`);
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
};
|
||||
abortSignal.addEventListener("abort", abortListener);
|
||||
@@ -65,12 +61,6 @@ export class PageService extends APIService {
|
||||
});
|
||||
|
||||
try {
|
||||
// The simulated delay that can be aborted
|
||||
// await Promise.race([
|
||||
// new Promise((resolve) => setTimeout(resolve, 10000)),
|
||||
// abortPromise
|
||||
// ]);
|
||||
|
||||
// The actual API call that can be aborted
|
||||
return await Promise.race([
|
||||
this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, data, {
|
||||
@@ -83,7 +73,6 @@ export class PageService extends APIService {
|
||||
.catch((error) => {
|
||||
// Special handling for aborted fetch requests
|
||||
if (error.name === "AbortError") {
|
||||
console.log(`Fetch request for title update was aborted: ${pageId}`);
|
||||
throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
throw error;
|
||||
|
||||
13
live/src/core/types/document-handler.d.ts
vendored
13
live/src/core/types/document-handler.d.ts
vendored
@@ -6,7 +6,6 @@ import { HocusPocusServerContext } from "@/core/types/common";
|
||||
export interface DocumentFetchParams {
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
params: URLSearchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,7 +15,6 @@ export interface DocumentStoreParams {
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
state: Uint8Array;
|
||||
params: URLSearchParams | undefined;
|
||||
title: string;
|
||||
}
|
||||
|
||||
@@ -37,22 +35,15 @@ export interface DocumentHandler {
|
||||
/**
|
||||
* Fetch title
|
||||
*/
|
||||
fetchTitle: (params: {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
pageId: string;
|
||||
cookie: string;
|
||||
}) => Promise<string | undefined>;
|
||||
fetchTitle: (params: { pageId: string; context: HocusPocusServerContext }) => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Update title
|
||||
*/
|
||||
updateTitle?: (params: {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
title: string;
|
||||
cookie: string;
|
||||
abortSignal?: AbortSignal;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
props;
|
||||
|
||||
return (
|
||||
<div className="frame-renderer flex-grow w-full">
|
||||
<div className={"frame-renderer flex-grow w-full space-y-4 document-editor-container"}>
|
||||
{titleEditor && (
|
||||
<div className="relative w-full py-3">
|
||||
<EditorContainer
|
||||
|
||||
@@ -46,7 +46,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
|
||||
const handleLinkHover = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!editor || editorState.linkExtensionStorage.isBubbleMenuOpen) return;
|
||||
if (!editor || editorState?.linkExtensionStorage?.isBubbleMenuOpen) return;
|
||||
|
||||
// Find the closest anchor tag from the event target
|
||||
const target = (event.target as HTMLElement)?.closest("a");
|
||||
@@ -109,7 +109,7 @@ export const LinkViewContainer: FC<LinkViewContainerProps> = ({ editor, containe
|
||||
|
||||
// Close link view when bubble menu opens
|
||||
useEffect(() => {
|
||||
if (editorState.linkExtensionStorage.isBubbleMenuOpen && isOpen) {
|
||||
if (editorState?.linkExtensionStorage?.isBubbleMenuOpen && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [editorState.linkExtensionStorage, isOpen]);
|
||||
|
||||
Reference in New Issue
Block a user