replace json.stringify with proper processing, fix small issues with Ephemral state, added worker (inactive)

This commit is contained in:
zsviczian
2024-08-25 16:08:12 +02:00
parent 8466c42217
commit e890e4489b
16 changed files with 265 additions and 31 deletions

View File

@@ -105,6 +105,7 @@ const BUILD_CONFIG = {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
}),
babel({
babelHelpers: 'bundled',
presets: [['@babel/preset-env', {
targets: {
ios: "15", // ios Compatibility //esmodules: true,

View File

@@ -49,6 +49,7 @@ import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { DEBUGGING, debug } from "./utils/DebugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -222,11 +223,12 @@ export function getMarkdownDrawingSection(
jsonString: string,
compressed: boolean,
) {
return compressed
const result = compressed
? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
jsonString,
)}\n\x60\x60\x60\n%%`
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
return result;
}
/**
@@ -1090,8 +1092,6 @@ export class ExcalidrawData {
return result;
}
let jsonString = JSON.stringify(this.scene);
let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
for (const el of elements) {
@@ -1102,11 +1102,10 @@ export class ExcalidrawData {
if (el.id.length > 8) {
result = true;
id = nanoid();
jsonString = jsonString.replaceAll(el.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.)
updateElementIdsInScene(this.scene, el, id);
}
this.elementLinks.set(id, el.link);
}
this.scene = JSON.parse(jsonString);
return result;
}
@@ -1118,9 +1117,7 @@ export class ExcalidrawData {
//console.log("Excalidraw.Data.findNewTextElementsInScene()");
//get scene text elements
this.selectedElementIds = selectedElementIds;
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
let jsonString = JSON.stringify(this.scene);
const texts = this.scene.elements?.filter((el: any) => el.type === "text") as ExcalidrawTextElement[];
let dirty: boolean = false; //to keep track if the json has changed
let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
@@ -1136,7 +1133,7 @@ export class ExcalidrawData {
delete this.selectedElementIds[te.id];
this.selectedElementIds[id] = true;
}
jsonString = jsonString.replaceAll(te.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.)
updateElementIdsInScene(this.scene, te, id);
if (this.textElements.has(te.id)) {
//element was created with onBeforeTextSubmit
const text = this.textElements.get(te.id);
@@ -1158,11 +1155,6 @@ export class ExcalidrawData {
}
}
if (dirty) {
//reload scene json in case it has changed
this.scene = JSON.parse(jsonString);
}
return dirty;
}
@@ -1470,7 +1462,7 @@ export class ExcalidrawData {
appState: this.scene.appState,
files: this.scene.files
}, null, "\t");
return (
const result = (
outString +
(this.textElementCommentedOut ? "" : "%%\n") +
getMarkdownDrawingSection(
@@ -1478,6 +1470,7 @@ export class ExcalidrawData {
this.disableCompression ? false : this.plugin.settings.compress,
)
);
return result;
}
public async saveDataURLtoVault(dataURL: DataURL, mimeType: MimeType, key: FileId) {
@@ -2027,7 +2020,7 @@ export class ExcalidrawData {
}
public getEquationEntries() {
return this.equations.entries();
return this.equations?.entries();
}
public deleteEquation(fileId: FileId) {

View File

@@ -404,7 +404,10 @@ export default class ExcalidrawView extends TextFileView {
preventAutozoom() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.preventAutozoom, "ExcalidrawView.preventAutozoom");
this.semaphores.preventAutozoom = true;
window.setTimeout(() => (this.semaphores.preventAutozoom = false), 1500);
window.setTimeout(() => {
if(!this.semaphores) return;
this.semaphores.preventAutozoom = false;
}, 1500);
}
public saveExcalidraw(scene?: any) {
@@ -734,10 +737,9 @@ export default class ExcalidrawView extends TextFileView {
return;
}
const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`);
try {
const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`);
if (allowSave) {
const scene = this.getScene();
@@ -2064,6 +2066,7 @@ export default class ExcalidrawView extends TextFileView {
if(images.length>0) {
this.preventAutozoom();
window.setTimeout(()=>this.zoomToElements(!api.getAppState().viewModeEnabled, images));
return;
}
}
}
@@ -2473,7 +2476,7 @@ export default class ExcalidrawView extends TextFileView {
*
* @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
*/
private async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) {
public async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadDrawing, "ExcalidrawView.loadDrawing", justloaded, deletedElements);
const excalidrawData = this.excalidrawData.scene;
this.semaphores.justLoaded = justloaded;

View File

@@ -7,7 +7,6 @@ import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;

View File

@@ -3,7 +3,6 @@ import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
destroy() {

View File

@@ -7,7 +7,6 @@ import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;

View File

@@ -5,7 +5,6 @@ import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
private drawingPath: string;

View File

@@ -5,7 +5,6 @@ import ExcalidrawPlugin from "../main";
import { getEA } from "src";
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
public app: App;
public plugin: ExcalidrawPlugin;
private view: ExcalidrawView;

View File

@@ -9,7 +9,6 @@ export enum openDialogAction {
}
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
public app: App;
private plugin: ExcalidrawPlugin;
private action: openDialogAction;
private onNewPane: boolean;

4
src/types/worker.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "web-worker:*" {
const WorkerFactory: new (options: any) => Worker;
export default WorkerFactory;
}

View File

@@ -14,6 +14,29 @@ export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => {
console.log(fnName, ...messages);
};
let timestamp: number[] = [];
let tsOrigin: number = 0;
export function tsInit(msg: string) {
tsOrigin = Date.now();
timestamp = [tsOrigin, tsOrigin, tsOrigin, tsOrigin, tsOrigin]; // Initialize timestamps for L0 to L4
console.log("0ms: " + msg);
}
export function ts(msg: string, level: number) {
if (level < 0 || level > 4) {
console.error("Invalid level. Please use level 0, 1, 2, 3, or 4.");
return;
}
const now = Date.now();
const diff = now - timestamp[level];
timestamp[level] = now;
const elapsedFromOrigin = now - tsOrigin;
console.log(`L${level} (${elapsedFromOrigin}ms) ${diff}ms: ${msg}`);
}
export class CustomMutationObserver {
private originalCallback: MutationCallback;
private observer: MutationObserver | null;

View File

@@ -0,0 +1,45 @@
import { ExcalidrawArrowElement, ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
export function updateElementIdsInScene(
{elements: sceneElements}: {elements: Mutable<ExcalidrawElement>[]},
elementToChange: Mutable<ExcalidrawElement>,
newID: string
) {
if(elementToChange.type === "text") {
const textElement = elementToChange as Mutable<ExcalidrawTextElement>;
if(textElement.containerId) {
const containerEl = sceneElements.find(el=>el.id === textElement.containerId) as unknown as Mutable<ExcalidrawElement>;
containerEl.boundElements?.filter(x=>x.id === textElement.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
}
}
if(elementToChange.boundElements?.length>0) {
elementToChange.boundElements.forEach( binding => {
const boundEl = sceneElements.find(el=>el.id === binding.id) as unknown as Mutable<ExcalidrawElement>;
boundEl.boundElements?.filter(x=>x.id === elementToChange.id).forEach( x => {
(x.id as Mutable<string>) = newID;
});
if(boundEl.type === "arrow") {
const arrow = boundEl as Mutable<ExcalidrawArrowElement>;
if(arrow.startBinding?.elementId === elementToChange.id) {
arrow.startBinding.elementId = newID;
}
if(arrow.endBinding?.elementId === elementToChange.id) {
arrow.endBinding.elementId = newID;
}
}
});
}
if(elementToChange.type === "frame") {
sceneElements.filter(el=>el.frameId === elementToChange.id).forEach(x => {
(x.frameId as Mutable<string>) = newID;
});
}
elementToChange.id = newID;
}

View File

@@ -529,11 +529,29 @@ export function getLinkParts (fname: string, file?: TFile): LinkParts {
};
export function compress (data: string): string {
return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n");
const compressed = LZString.compressToBase64(data);
let result = '';
const chunkSize = 256;
for (let i = 0; i < compressed.length; i += chunkSize) {
result += compressed.slice(i, i + chunkSize) + '\n\n';
}
return result.trim();
};
export function decompress (data: string): string {
return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", ""));
let cleanedData = '';
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data[i];
if (char !== '\n' && char !== '\r') {
cleanedData += char;
}
}
return LZString.decompressFromBase64(cleanedData);
};
export function isMaskFile (

153
src/workers/save-worker.ts Normal file
View File

@@ -0,0 +1,153 @@
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import ExcalidrawView from "src/ExcalidrawView";
import { debug, DEBUGGING, ts } from "src/utils/DebugHelper";
import { imageCache } from "src/utils/ImageCache";
interface SaveWorkerMessageData {
superSave: () => Promise<void>;
view: ExcalidrawView;
forcesave: boolean;
preventReload: boolean;
}
onmessage = async function (e: MessageEvent<SaveWorkerMessageData>) {
const { superSave, view, forcesave, preventReload } = e.data;
try {
(process.env.NODE_ENV === 'development') && ts("allow save",1);
const scene = view.getScene();
if (view.compatibilityMode) {
await view.excalidrawData.syncElements(scene);
} else if (
await view.excalidrawData.syncElements(scene, view.excalidrawAPI.getAppState().selectedElementIds)
&& !view.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout
) {
(process.env.NODE_ENV === 'development') && ts("ExcalidrawView.beforeLoadDrawing",1);
await view.loadDrawing(
false,
view.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted)
);
(process.env.NODE_ENV === 'development') && ts("ExcalidrawView.afterLoadDrawing",1);
}
(process.env.NODE_ENV === 'development') && ts("after sync elements",1);
//reload() is triggered indirectly when saving by the modifyEventHandler in main.ts
//prevent reload is set here to override reload when not wanted: typically when the user is editing
//and we do not want to interrupt the flow by reloading the drawing into the canvas.
view.clearDirty();
view.clearPreventReloadTimer();
view.semaphores.preventReload = preventReload;
//added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back
//can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607
if(view.semaphores?.viewunload) {
const d = view.getViewData();
const plugin = view.plugin;
const file = view.file;
window.setTimeout(async ()=>{
await plugin.app.vault.modify(file,d);
await imageCache.addBAKToCache(file.path,d);
},200)
return;
}
(process.env.NODE_ENV === 'development') && ts("before super.save",1);
await superSave();
(process.env.NODE_ENV === 'development') && ts("after super.save",1);
if (process.env.NODE_ENV === 'development') {
if (DEBUGGING) {
debug(self.onmessage, `ExcalidrawView.save, super.save finished`, view.file);
console.trace();
}
}
//saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted.
const path = view.file.path;
//@ts-ignore
const data = view.lastSavedData;
window.setTimeout(()=>imageCache.addBAKToCache(path,data),50);
const triggerReload = (view.lastSaveTimestamp === view.file.stat.mtime) &&
!preventReload && forcesave;
view.lastSaveTimestamp = view.file.stat.mtime;
//view.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629
//there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image
//did not update the open drawing
if(preventReload) {
view.setPreventReload();
}
postMessage({ success: true, triggerReload });
} catch (error) {
postMessage({ success: false, error: (error as Error).message });
}
};
/* ExcalidrawView
import SaveWorker from "web-worker:./workers/save-worker.ts";
//...
try {
if (allowSave) {
const worker = new SaveWorker({name: "Excalidraw File Save Worker"});
const promise = new Promise<{success:boolean, error?:Error, triggerReload?: boolean}>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<{ success: boolean; error?: Error; triggerReload?: boolean }>) => {
resolve(e.data);
};
worker.onerror = (e: ErrorEvent) => {
reject(new Error(e.message));
};
worker.postMessage({
superSave: super.save.bind(this),
view: this,
preventReload,
forcesave,
});
});
const { success, error, triggerReload: tr } = await promise;
worker.terminate();
if(error) {
throw error;
}
if(typeof tr !== "undefined") {
triggerReload = tr;
}
}
// !triggerReload means file has not changed. No need to re-export
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition)
if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) {
const autoexportPreference = this.excalidrawData.autoexportPreference;
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg
) {
this.saveSVG();
}
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) ||
autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png
) {
this.savePNG();
}
if (
!this.compatibilityMode &&
this.plugin.settings.autoexportExcalidraw
) {
this.saveExcalidraw();
}
}
} catch (e) {
errorlog({
where: "ExcalidrawView.save",
fn: this.save,
error: e,
});
warningUnknowSeriousError();
}
*/

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "ES2015",
"module": "es2015",
"target": "es2018", //es2017 because script engine requires for async execution
"allowJs": true,
"noImplicitAny": true,

View File

@@ -14,7 +14,7 @@
"dom",
"scripthost",
"es2015",
"ESNext",
"esnext",
"DOM.Iterable"
],
"jsx": "react",