fix: Small race conditions in diagrams.net integration (#11458)

This commit is contained in:
Tom Moor
2026-02-15 12:45:40 -05:00
committed by GitHub
parent 45a19d52cf
commit 08d58f7a6d
+62 -24
View File
@@ -12,6 +12,16 @@ import {
} from "../lib/DiagramsNetClient";
import { sanitizeUrl } from "../../utils/urls";
/**
* Tracks the mutable state for a single diagram editing session. Callbacks
* close over a session object so that concurrent or overlapping sessions
* do not interfere with each other.
*/
interface DiagramSession {
/** The current src used to locate the node in the document. Updated after each successful export. */
nodeSrc: string;
}
/**
* An editor extension that adds commands to insert and edit diagrams using diagrams.net.
*
@@ -27,6 +37,10 @@ export default class Diagrams extends Extension {
commands(): Record<string, CommandFactory> {
return {
editDiagram: (): Command => (state, dispatch) => {
if (!dispatch) {
return true;
}
const selectedNode = this.getSelectedImageNode(state);
if (!selectedNode) {
@@ -61,7 +75,10 @@ export default class Diagrams extends Extension {
* @param state - the editor state.
* @param dispatch - the dispatch function.
*/
private insertEmptyDiagram(state: EditorState, dispatch?: any) {
private insertEmptyDiagram(
state: EditorState,
dispatch: (tr: ReturnType<EditorState["tr"]["insert"]>) => void
) {
const type = this.editor.schema.nodes.image;
const { tr } = state;
const transaction = tr.insert(
@@ -71,7 +88,7 @@ export default class Diagrams extends Extension {
source: ImageSource.DiagramsNet,
})
);
dispatch?.(transaction);
dispatch(transaction);
}
/**
@@ -80,18 +97,21 @@ export default class Diagrams extends Extension {
* @param node - the selected image node, if any.
*/
private openDiagramEditor(node?: Node) {
this.currentNodeSrc = node?.attrs.src ?? "";
const sourceUrl = this.currentNodeSrc || EMPTY_DIAGRAM_IMAGE;
const nodeSrc = node?.attrs.src ?? "";
const sourceUrl = nodeSrc || EMPTY_DIAGRAM_IMAGE;
// Create a per-session object. Async callbacks close over this object so
// that a second editing session does not clobber the first session's state.
const session: DiagramSession = { nodeSrc };
// Clean up any existing client
if (this.client) {
this.client.close();
}
// Create new client with callbacks
this.client = new DiagramsNetClient(
() => this.onDiagramReady(sourceUrl),
(base64Data) => this.onDiagramExported(base64Data)
(client) => this.onDiagramReady(client, sourceUrl),
(base64Data) => this.onDiagramExported(base64Data, session)
);
this.client.open(this.getDiagramsNetUrl());
@@ -100,9 +120,10 @@ export default class Diagrams extends Extension {
/**
* Called when the diagram editor is ready to receive commands.
*
* @param client - the DiagramsNetClient that fired the ready event.
* @param sourceUrl - the URL of the diagram to load, or the empty diagram constant.
*/
private async onDiagramReady(sourceUrl: string) {
private async onDiagramReady(client: DiagramsNetClient, sourceUrl: string) {
let data: string;
if (sourceUrl === EMPTY_DIAGRAM_IMAGE) {
@@ -113,25 +134,38 @@ export default class Diagrams extends Extension {
data = await FileHelper.urlToBase64(sourceUrl);
}
this.client.loadDiagram(data);
client.loadDiagram(data);
}
/**
* Called when a diagram has been exported from the editor.
*
* @param base64Data - the exported diagram as base64 encoded SVG.
* @param session - the editing session that produced this export.
*/
private async onDiagramExported(base64Data: string) {
const file = FileHelper.base64ToFile(
base64Data,
"diagram.svg",
"image/svg+xml"
);
const dimensions = await FileHelper.getImageDimensions(file);
const uploadedUrl = await this.uploadDiagramFile(file);
private async onDiagramExported(base64Data: string, session: DiagramSession) {
try {
const file = FileHelper.base64ToFile(
base64Data,
"diagram.svg",
"image/svg+xml"
);
this.updateDiagramInDocument(uploadedUrl, dimensions || {});
this.currentNodeSrc = uploadedUrl;
const dimensions = await FileHelper.getImageDimensions(file);
const uploadedUrl = await this.uploadDiagramFile(file);
// Capture the src we need to search for *before* updating the session,
// then update the document and the session atomically.
const srcToFind = session.nodeSrc;
this.updateDiagramInDocument(uploadedUrl, dimensions || {}, srcToFind);
// Update session so that subsequent saves within the same editing session
// can locate the node by its new uploaded URL.
session.nodeSrc = uploadedUrl;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Failed to export diagram:", error);
}
}
/**
@@ -150,21 +184,26 @@ export default class Diagrams extends Extension {
}
/**
* Updates or inserts the diagram image in the document.
* Updates or inserts the diagram image in the document. Always reads fresh
* editor state at call-time so that positions are accurate even after async
* gaps.
*
* @param uploadedUrl - the URL of the uploaded diagram.
* @param dimensions - the image dimensions.
* @param srcToFind - the src attribute value to search for in the document.
*/
private updateDiagramInDocument(
uploadedUrl: string,
dimensions: { width?: number; height?: number }
dimensions: { width?: number; height?: number },
srcToFind: string
) {
// Read fresh state at the moment of dispatch to avoid stale positions.
const { state } = this.editor.view;
const { dispatch } = this.editor.view;
const imageType = this.editor.schema.nodes.image;
// Try to find and update existing node
const existingNode = this.findImageNodeBySrc(state, this.currentNodeSrc);
// Try to find and update the existing node by its current src attribute.
const existingNode = this.findImageNodeBySrc(state, srcToFind);
const attrs = {
...dimensions,
@@ -226,5 +265,4 @@ export default class Diagrams extends Extension {
}
private client: DiagramsNetClient;
private currentNodeSrc: string = "";
}