mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -08:00
Fix/watch fs large json (#657)
* fix: large json import still need to check identity * fix: show wiki server error by hide view * fix: possible loop when image load failed fixes https://github.com/tiddly-gittly/TidGi-Desktop/issues/562 * fix: more check and reduce logs * fix: sub wiki no sync notification fixes https://github.com/tiddly-gittly/TidGi-Desktop/issues/526 * fix: type * fix: update git sync js fixes https://github.com/tiddly-gittly/TidGi-Desktop/issues/515 * fix: msix not uploaded * typo * lint * Update comparison.ts
This commit is contained in:
parent
ccf825af06
commit
45e3f76da1
14 changed files with 248 additions and 108 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -134,7 +134,7 @@ jobs:
|
||||||
draft: true
|
draft: true
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
${{ matrix.platform == 'win' && 'out/make/**/*.exe' || 'out/make/**/*' }}
|
${{ matrix.platform == 'win' && 'out/make/**/*.{exe,msix,appx}' || 'out/make/**/*' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
"espree": "^11.0.0",
|
"espree": "^11.0.0",
|
||||||
"exponential-backoff": "^3.1.3",
|
"exponential-backoff": "^3.1.3",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "11.3.2",
|
||||||
"git-sync-js": "^2.3.0",
|
"git-sync-js": "^2.3.1",
|
||||||
"graphql-hooks": "8.2.0",
|
"graphql-hooks": "8.2.0",
|
||||||
"html-minifier-terser": "^7.2.0",
|
"html-minifier-terser": "^7.2.0",
|
||||||
"i18next": "25.6.3",
|
"i18next": "25.6.3",
|
||||||
|
|
@ -129,6 +129,7 @@
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@electron-forge/maker-deb": "7.10.2",
|
"@electron-forge/maker-deb": "7.10.2",
|
||||||
"@electron-forge/maker-flatpak": "7.10.2",
|
"@electron-forge/maker-flatpak": "7.10.2",
|
||||||
|
"@electron-forge/maker-msix": "7.10.2",
|
||||||
"@electron-forge/maker-rpm": "7.10.2",
|
"@electron-forge/maker-rpm": "7.10.2",
|
||||||
"@electron-forge/maker-snap": "7.10.2",
|
"@electron-forge/maker-snap": "7.10.2",
|
||||||
"@electron-forge/maker-squirrel": "7.10.2",
|
"@electron-forge/maker-squirrel": "7.10.2",
|
||||||
|
|
@ -138,7 +139,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cucumber/cucumber": "^12.2.0",
|
"@cucumber/cucumber": "^12.2.0",
|
||||||
"@electron-forge/cli": "7.10.2",
|
"@electron-forge/cli": "7.10.2",
|
||||||
"@electron-forge/maker-msix": "^7.10.2",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.10.2",
|
"@electron-forge/plugin-auto-unpack-natives": "7.10.2",
|
||||||
"@electron-forge/plugin-vite": "7.10.2",
|
"@electron-forge/plugin-vite": "7.10.2",
|
||||||
"@electron/rebuild": "^4.0.1",
|
"@electron/rebuild": "^4.0.1",
|
||||||
|
|
|
||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
|
|
@ -138,8 +138,8 @@ importers:
|
||||||
specifier: 11.3.2
|
specifier: 11.3.2
|
||||||
version: 11.3.2
|
version: 11.3.2
|
||||||
git-sync-js:
|
git-sync-js:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.1
|
||||||
version: 2.3.0
|
version: 2.3.1
|
||||||
graphql-hooks:
|
graphql-hooks:
|
||||||
specifier: 8.2.0
|
specifier: 8.2.0
|
||||||
version: 8.2.0(react@19.2.0)
|
version: 8.2.0(react@19.2.0)
|
||||||
|
|
@ -297,9 +297,6 @@ importers:
|
||||||
'@electron-forge/cli':
|
'@electron-forge/cli':
|
||||||
specifier: 7.10.2
|
specifier: 7.10.2
|
||||||
version: 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.27.0)
|
version: 7.10.2(@swc/core@1.12.0)(bluebird@3.7.2)(encoding@0.1.13)(esbuild@0.27.0)
|
||||||
'@electron-forge/maker-msix':
|
|
||||||
specifier: ^7.10.2
|
|
||||||
version: 7.10.2(bluebird@3.7.2)
|
|
||||||
'@electron-forge/plugin-auto-unpack-natives':
|
'@electron-forge/plugin-auto-unpack-natives':
|
||||||
specifier: 7.10.2
|
specifier: 7.10.2
|
||||||
version: 7.10.2(bluebird@3.7.2)
|
version: 7.10.2(bluebird@3.7.2)
|
||||||
|
|
@ -448,6 +445,9 @@ importers:
|
||||||
'@electron-forge/maker-flatpak':
|
'@electron-forge/maker-flatpak':
|
||||||
specifier: 7.10.2
|
specifier: 7.10.2
|
||||||
version: 7.10.2(bluebird@3.7.2)
|
version: 7.10.2(bluebird@3.7.2)
|
||||||
|
'@electron-forge/maker-msix':
|
||||||
|
specifier: 7.10.2
|
||||||
|
version: 7.10.2(bluebird@3.7.2)
|
||||||
'@electron-forge/maker-rpm':
|
'@electron-forge/maker-rpm':
|
||||||
specifier: 7.10.2
|
specifier: 7.10.2
|
||||||
version: 7.10.2(bluebird@3.7.2)
|
version: 7.10.2(bluebird@3.7.2)
|
||||||
|
|
@ -4572,8 +4572,8 @@ packages:
|
||||||
gifwrap@0.10.1:
|
gifwrap@0.10.1:
|
||||||
resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==}
|
resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==}
|
||||||
|
|
||||||
git-sync-js@2.3.0:
|
git-sync-js@2.3.1:
|
||||||
resolution: {integrity: sha512-RFyyWBjdZskl2Jxg3a9g2tzAShhhFkxyS83lON6asaKHov4Rks8P7irCMjQIRiFTlqLrvZFDgjeWbo7jc1IKhQ==}
|
resolution: {integrity: sha512-EbX6SIsUc2hcXEQXsaYz0EyDSQwlKi3AbZk00hIB97YOyKQPtKeQWQ7+6w87GSKD6m2mfBGVyIgF57DA0ajo3g==}
|
||||||
|
|
||||||
github-from-package@0.0.0:
|
github-from-package@0.0.0:
|
||||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
@ -8456,6 +8456,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bluebird
|
- bluebird
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@electron-forge/maker-rpm@7.10.2(bluebird@3.7.2)':
|
'@electron-forge/maker-rpm@7.10.2(bluebird@3.7.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -11122,6 +11123,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -11650,6 +11652,7 @@ snapshots:
|
||||||
xml-escape: 1.1.0
|
xml-escape: 1.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
electron-winstaller@5.4.0:
|
electron-winstaller@5.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -12657,7 +12660,7 @@ snapshots:
|
||||||
image-q: 4.0.0
|
image-q: 4.0.0
|
||||||
omggif: 1.0.10
|
omggif: 1.0.10
|
||||||
|
|
||||||
git-sync-js@2.3.0:
|
git-sync-js@2.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
dugite: 3.0.0-rc12
|
dugite: 3.0.0-rc12
|
||||||
fs-extra: 11.3.2
|
fs-extra: 11.3.2
|
||||||
|
|
@ -15844,7 +15847,8 @@ snapshots:
|
||||||
|
|
||||||
ws@8.18.3: {}
|
ws@8.18.3: {}
|
||||||
|
|
||||||
xml-escape@1.1.0: {}
|
xml-escape@1.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
xml-name-validator@5.0.0: {}
|
xml-name-validator@5.0.0: {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,8 @@ function initWikiGit(
|
||||||
*/
|
*/
|
||||||
function commitAndSyncWiki(workspace: IWorkspace, configs: ICommitAndSyncConfigs, errorI18NDict: Record<string, string>): Observable<IGitLogMessage> {
|
function commitAndSyncWiki(workspace: IWorkspace, configs: ICommitAndSyncConfigs, errorI18NDict: Record<string, string>): Observable<IGitLogMessage> {
|
||||||
return new Observable<IGitLogMessage>((observer) => {
|
return new Observable<IGitLogMessage>((observer) => {
|
||||||
|
// For sub-wiki, show sync progress in main workspace
|
||||||
|
const workspaceIDForNotification = isWikiWorkspace(workspace) && workspace.isSubWiki ? workspace.mainWikiID! : workspace.id;
|
||||||
void commitAndSync({
|
void commitAndSync({
|
||||||
...configs,
|
...configs,
|
||||||
defaultGitInfo,
|
defaultGitInfo,
|
||||||
|
|
@ -113,7 +115,7 @@ function commitAndSyncWiki(workspace: IWorkspace, configs: ICommitAndSyncConfigs
|
||||||
observer.next({ message, level: 'warn', meta: { callerFunction: 'commitAndSync', ...context } });
|
observer.next({ message, level: 'warn', meta: { callerFunction: 'commitAndSync', ...context } });
|
||||||
},
|
},
|
||||||
info: (message: GitStep, context: ILoggerContext): void => {
|
info: (message: GitStep, context: ILoggerContext): void => {
|
||||||
observer.next({ message, level: 'info', meta: { handler: WikiChannel.syncProgress, id: workspace.id, callerFunction: 'commitAndSync', ...context } });
|
observer.next({ message, level: 'info', meta: { handler: WikiChannel.syncProgress, id: workspaceIDForNotification, callerFunction: 'commitAndSync', ...context } });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
filesToIgnore: ['.DS_Store'],
|
filesToIgnore: ['.DS_Store'],
|
||||||
|
|
@ -146,6 +148,8 @@ function forcePullWiki(workspace: IWorkspace, configs: IForcePullConfigs, errorI
|
||||||
observer.error(new Error('forcePullWiki can only be called on wiki workspaces'));
|
observer.error(new Error('forcePullWiki can only be called on wiki workspaces'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// For sub-wiki, show sync progress in main workspace
|
||||||
|
const workspaceIDForNotification = isWikiWorkspace(workspace) && workspace.isSubWiki ? workspace.mainWikiID! : workspace.id;
|
||||||
void forcePull({
|
void forcePull({
|
||||||
dir: workspace.wikiFolderLocation,
|
dir: workspace.wikiFolderLocation,
|
||||||
...configs,
|
...configs,
|
||||||
|
|
@ -158,7 +162,7 @@ function forcePullWiki(workspace: IWorkspace, configs: IForcePullConfigs, errorI
|
||||||
observer.next({ message, level: 'warn', meta: { callerFunction: 'forcePull', ...context } });
|
observer.next({ message, level: 'warn', meta: { callerFunction: 'forcePull', ...context } });
|
||||||
},
|
},
|
||||||
info: (message: GitStep, context: ILoggerContext): void => {
|
info: (message: GitStep, context: ILoggerContext): void => {
|
||||||
observer.next({ message, level: 'info', meta: { handler: WikiChannel.syncProgress, id: workspace.id, callerFunction: 'forcePull', ...context } });
|
observer.next({ message, level: 'info', meta: { handler: WikiChannel.syncProgress, id: workspaceIDForNotification, callerFunction: 'forcePull', ...context } });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).then(
|
}).then(
|
||||||
|
|
|
||||||
|
|
@ -428,14 +428,14 @@ ${message.message}
|
||||||
}
|
}
|
||||||
|
|
||||||
public formatFileUrlToAbsolutePath(urlWithFileProtocol: string): string {
|
public formatFileUrlToAbsolutePath(urlWithFileProtocol: string): string {
|
||||||
logger.info('getting url', { url: urlWithFileProtocol, function: 'formatFileUrlToAbsolutePath' });
|
logger.debug('formatting file URL to absolute path', { url: urlWithFileProtocol, function: 'formatFileUrlToAbsolutePath' });
|
||||||
let pathname = '';
|
let pathname = '';
|
||||||
let hostname = '';
|
let hostname = '';
|
||||||
try {
|
try {
|
||||||
({ hostname, pathname } = new URL(urlWithFileProtocol));
|
({ hostname, pathname } = new URL(urlWithFileProtocol));
|
||||||
} catch {
|
} catch {
|
||||||
pathname = urlWithFileProtocol.replace('file://', '').replace('open://', '');
|
pathname = urlWithFileProtocol.replace('file://', '').replace('open://', '');
|
||||||
logger.error(`Parse URL failed, use original url replace file:// instead`, { pathname, function: 'formatFileUrlToAbsolutePath.error' });
|
logger.debug(`Parse URL failed, using fallback string replace`, { pathname, function: 'formatFileUrlToAbsolutePath' });
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* urlWithFileProtocol: `file://./files/xxx.png`
|
* urlWithFileProtocol: `file://./files/xxx.png`
|
||||||
|
|
@ -446,38 +446,33 @@ ${message.message}
|
||||||
if (process.platform === 'win32' && filePath.startsWith('/')) {
|
if (process.platform === 'win32' && filePath.startsWith('/')) {
|
||||||
filePath = filePath.substring(1);
|
filePath = filePath.substring(1);
|
||||||
}
|
}
|
||||||
logger.info('handle file:// or open:// This url will open file in-wiki', { hostname, pathname, filePath, function: 'formatFileUrlToAbsolutePath' });
|
|
||||||
let fileExists = fs.existsSync(filePath);
|
// Strategy 1: Try as-is (for absolute paths)
|
||||||
logger.info('file exists (decodeURI)', {
|
if (fs.existsSync(filePath)) {
|
||||||
function: 'formatFileUrlToAbsolutePath',
|
logger.debug('file found (direct path)', { filePath, function: 'formatFileUrlToAbsolutePath' });
|
||||||
filePath,
|
|
||||||
exists: fileExists,
|
|
||||||
});
|
|
||||||
if (fileExists) {
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
logger.info(`try find file relative to workspace folder`, { filePath, function: 'formatFileUrlToAbsolutePath' });
|
|
||||||
|
// Strategy 2: Try relative to workspace folder
|
||||||
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
|
||||||
const workspace = workspaceService.getActiveWorkspaceSync();
|
const workspace = workspaceService.getActiveWorkspaceSync();
|
||||||
if (workspace === undefined || !isWikiWorkspace(workspace)) {
|
if (workspace !== undefined && isWikiWorkspace(workspace)) {
|
||||||
logger.error(`No active workspace or not a wiki workspace, abort. Try loading filePath as-is.`, { filePath, function: 'formatFileUrlToAbsolutePath' });
|
const filePathInWorkspaceFolder = path.resolve(workspace.wikiFolderLocation, filePath);
|
||||||
return filePath;
|
if (fs.existsSync(filePathInWorkspaceFolder)) {
|
||||||
|
logger.debug('file found (workspace relative)', { filePathInWorkspaceFolder, function: 'formatFileUrlToAbsolutePath' });
|
||||||
|
return filePathInWorkspaceFolder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// try concat workspace path + file path to get relative path
|
|
||||||
const filePathInWorkspaceFolder = path.resolve(workspace.wikiFolderLocation, filePath);
|
// Strategy 3: Try relative to TidGi App folder (for bundled assets)
|
||||||
fileExists = fs.existsSync(filePathInWorkspaceFolder);
|
|
||||||
logger.info(`This file ${fileExists ? '' : 'not '}exists in workspace folder.`, { filePathInWorkspaceFolder, function: 'formatFileUrlToAbsolutePath' });
|
|
||||||
if (fileExists) {
|
|
||||||
return filePathInWorkspaceFolder;
|
|
||||||
}
|
|
||||||
// on production, __dirname will be in .webpack/main
|
|
||||||
const inTidGiAppAbsoluteFilePath = path.join(app.getAppPath(), '.webpack', 'renderer', filePath);
|
const inTidGiAppAbsoluteFilePath = path.join(app.getAppPath(), '.webpack', 'renderer', filePath);
|
||||||
logger.info(`try find file relative to TidGi App folder`, { inTidGiAppAbsoluteFilePath, function: 'formatFileUrlToAbsolutePath' });
|
if (fs.existsSync(inTidGiAppAbsoluteFilePath)) {
|
||||||
fileExists = fs.existsSync(inTidGiAppAbsoluteFilePath);
|
logger.debug('file found (app relative)', { inTidGiAppAbsoluteFilePath, function: 'formatFileUrlToAbsolutePath' });
|
||||||
if (fileExists) {
|
|
||||||
return inTidGiAppAbsoluteFilePath;
|
return inTidGiAppAbsoluteFilePath;
|
||||||
}
|
}
|
||||||
logger.warn(`This url can't be loaded in-wiki. Try loading url as-is.`, { url: urlWithFileProtocol, function: 'formatFileUrlToAbsolutePath' });
|
|
||||||
|
// File not found - return original URL as fallback
|
||||||
|
logger.warn('file not found in any location, returning original URL', { url: urlWithFileProtocol, filePath, function: 'formatFileUrlToAbsolutePath' });
|
||||||
return urlWithFileProtocol;
|
return urlWithFileProtocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,25 +72,60 @@ export function handleViewFileContentLoading(view: WebContentsView) {
|
||||||
|
|
||||||
function handleFileLink(details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) {
|
function handleFileLink(details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) {
|
||||||
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
|
const nativeService = container.get<INativeService>(serviceIdentifier.NativeService);
|
||||||
const absolutePath: string | undefined = nativeService.formatFileUrlToAbsolutePath(details.url);
|
const absolutePath: string = nativeService.formatFileUrlToAbsolutePath(details.url);
|
||||||
// When details.url is an absolute route, we just load it, don't need any redirect
|
|
||||||
|
// Prevent infinite redirect loop when path resolution failed
|
||||||
|
// formatFileUrlToAbsolutePath already checks file existence internally (3 times with different path strategies)
|
||||||
|
// If file not found, it returns the original URL as fallback
|
||||||
|
// Case 1: formatFileUrlToAbsolutePath returns the original URL (file not found fallback)
|
||||||
|
// Case 2: Resolved path still contains protocol (resolution failed)
|
||||||
|
// Case 3: Resolved path is relative (./xxx or ../xxx) - these cannot be loaded by Electron
|
||||||
if (
|
if (
|
||||||
`file://${absolutePath}` === decodeURI(details.url) ||
|
absolutePath === details.url ||
|
||||||
absolutePath === decodeURI(details.url) ||
|
absolutePath.startsWith('file://') ||
|
||||||
// also allow malformed `file:///` on `details.url` on windows, prevent infinite redirect when this check failed.
|
absolutePath.startsWith('open://') ||
|
||||||
(process.platform === 'win32' && `file:///${absolutePath}` === decodeURI(details.url))
|
absolutePath.startsWith('./') ||
|
||||||
|
absolutePath.startsWith('../')
|
||||||
) {
|
) {
|
||||||
logger.debug('open file protocol', {
|
logger.warn('File path resolution failed or returned invalid path, request canceled to prevent redirect loop', {
|
||||||
function: 'handleFileLink',
|
function: 'handleFileLink',
|
||||||
absolutePath: absolutePath ?? '',
|
originalUrl: details.url,
|
||||||
|
resolvedPath: absolutePath,
|
||||||
|
reason: absolutePath === details.url
|
||||||
|
? 'same as original'
|
||||||
|
: absolutePath.startsWith('file://') || absolutePath.startsWith('open://')
|
||||||
|
? 'contains protocol'
|
||||||
|
: 'relative path',
|
||||||
|
});
|
||||||
|
callback({
|
||||||
|
cancel: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When details.url is already an absolute file path, load it directly without redirect
|
||||||
|
const decodedUrl = decodeURI(details.url);
|
||||||
|
if (
|
||||||
|
`file://${absolutePath}` === decodedUrl ||
|
||||||
|
absolutePath === decodedUrl ||
|
||||||
|
// also allow malformed `file:///` on `details.url` on windows
|
||||||
|
(process.platform === 'win32' && `file:///${absolutePath}` === decodedUrl)
|
||||||
|
) {
|
||||||
|
logger.debug('Loading file without redirect', {
|
||||||
|
function: 'handleFileLink',
|
||||||
|
absolutePath,
|
||||||
|
originalUrl: details.url,
|
||||||
});
|
});
|
||||||
callback({
|
callback({
|
||||||
cancel: false,
|
cancel: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.info('redirecting file protocol', {
|
// Need to redirect relative path to absolute path
|
||||||
|
logger.info('Redirecting file protocol to absolute path', {
|
||||||
function: 'handleFileLink',
|
function: 'handleFileLink',
|
||||||
absolutePath: absolutePath ?? '',
|
originalUrl: details.url,
|
||||||
|
absolutePath,
|
||||||
|
redirectURL: `file://${absolutePath}`,
|
||||||
});
|
});
|
||||||
callback({
|
callback({
|
||||||
cancel: false,
|
cancel: false,
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,15 @@ export class Wiki implements IWikiService {
|
||||||
function: 'startWiki',
|
function: 'startWiki',
|
||||||
});
|
});
|
||||||
await workspaceService.updateMetaData(workspaceID, { isLoading: false, didFailLoadErrorMessage: errorMessage });
|
await workspaceService.updateMetaData(workspaceID, { isLoading: false, didFailLoadErrorMessage: errorMessage });
|
||||||
|
|
||||||
|
// For plugin errors that occur after wiki boot, realign the view to hide it and show error message
|
||||||
|
const isPluginError = message.source === 'plugin-error';
|
||||||
|
if (isPluginError && workspace.active) {
|
||||||
|
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
|
||||||
|
await workspaceViewService.realignActiveWorkspace(workspaceID);
|
||||||
|
logger.info('Realigned view after plugin error', { workspaceID, function: 'startWiki' });
|
||||||
|
}
|
||||||
|
|
||||||
// fix "message":"listen EADDRINUSE: address already in use 0.0.0.0:5212"
|
// fix "message":"listen EADDRINUSE: address already in use 0.0.0.0:5212"
|
||||||
if (errorMessage.includes('EADDRINUSE')) {
|
if (errorMessage.includes('EADDRINUSE')) {
|
||||||
const portChange = {
|
const portChange = {
|
||||||
|
|
@ -253,7 +262,11 @@ export class Wiki implements IWikiService {
|
||||||
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, true, { ...workspace, ...portChange }));
|
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, true, { ...workspace, ...portChange }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, false, { ...workspace }));
|
|
||||||
|
// For plugin errors, don't reject - let user see the error and try to recover
|
||||||
|
if (!isPluginError) {
|
||||||
|
reject(new WikiRuntimeError(new Error(message.message), wikiFolderLocation, false, { ...workspace }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import fs from 'fs';
|
||||||
import nsfw from 'nsfw';
|
import nsfw from 'nsfw';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
||||||
|
import { hasMeaningfulChanges } from './comparison';
|
||||||
import { FileSystemAdaptor } from './FileSystemAdaptor';
|
import { FileSystemAdaptor } from './FileSystemAdaptor';
|
||||||
import { type IBootFilesIndexItemWithTitle, InverseFilesIndex } from './InverseFilesIndex';
|
import { type IBootFilesIndexItemWithTitle, InverseFilesIndex } from './InverseFilesIndex';
|
||||||
|
|
||||||
|
|
@ -623,8 +624,20 @@ export class WatchFileSystemAdaptor extends FileSystemAdaptor {
|
||||||
tiddlerTitle,
|
tiddlerTitle,
|
||||||
} as IBootFilesIndexItemWithTitle);
|
} as IBootFilesIndexItemWithTitle);
|
||||||
|
|
||||||
// Add tiddler to wiki (this will update if it exists or add if new)
|
// Get existing tiddler to check if content actually changed
|
||||||
const syncAdaptor = $tw.syncadaptor as { wiki: Wiki } | undefined | null;
|
const syncAdaptor = $tw.syncadaptor as { wiki: Wiki } | undefined | null;
|
||||||
|
const existingTiddler = syncAdaptor?.wiki.getTiddler(tiddlerTitle);
|
||||||
|
|
||||||
|
// Use content comparison to prevent unnecessary saves
|
||||||
|
// This is critical for large JSON imports that would otherwise cause infinite loops
|
||||||
|
if (existingTiddler) {
|
||||||
|
if (!hasMeaningfulChanges(existingTiddler.fields, tiddler)) {
|
||||||
|
// Content is identical, skip addTiddler to prevent save cycle
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tiddler to wiki (this will trigger save if content changed)
|
||||||
syncAdaptor?.wiki.addTiddler(tiddler);
|
syncAdaptor?.wiki.addTiddler(tiddler);
|
||||||
|
|
||||||
// Log appropriate event
|
// Log appropriate event
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { workspace } from '@services/wiki/wikiWorker/services';
|
import { workspace } from '@services/wiki/wikiWorker/services';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { FileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ global.$tw = {
|
||||||
node: true,
|
node: true,
|
||||||
boot: {
|
boot: {
|
||||||
wikiTiddlersPath: '/test/wiki/tiddlers',
|
wikiTiddlersPath: '/test/wiki/tiddlers',
|
||||||
files: {} as Record<string, FileInfo>,
|
files: {} as Record<string, IFileInfo>,
|
||||||
},
|
},
|
||||||
utils: mockUtils,
|
utils: mockUtils,
|
||||||
};
|
};
|
||||||
|
|
@ -139,7 +139,7 @@ describe('FileSystemAdaptor - Basic Functionality', () => {
|
||||||
|
|
||||||
describe('getTiddlerInfo', () => {
|
describe('getTiddlerInfo', () => {
|
||||||
it('should return file info for existing tiddler', () => {
|
it('should return file info for existing tiddler', () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { workspace } from '@services/wiki/wikiWorker/services';
|
import { workspace } from '@services/wiki/wikiWorker/services';
|
||||||
import type { FileInfo, Wiki } from 'tiddlywiki';
|
import type { IFileInfo, Wiki } from 'tiddlywiki';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ global.$tw = {
|
||||||
node: true,
|
node: true,
|
||||||
boot: {
|
boot: {
|
||||||
wikiTiddlersPath: '/test/wiki/tiddlers',
|
wikiTiddlersPath: '/test/wiki/tiddlers',
|
||||||
files: {} as Record<string, FileInfo>,
|
files: {} as Record<string, IFileInfo>,
|
||||||
},
|
},
|
||||||
utils: mockUtils,
|
utils: mockUtils,
|
||||||
};
|
};
|
||||||
|
|
@ -73,7 +73,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
|
|
||||||
describe('deleteTiddler - Callback Mode', () => {
|
describe('deleteTiddler - Callback Mode', () => {
|
||||||
it('should delete tiddler and call callback on success', async () => {
|
it('should delete tiddler and call callback on success', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -105,7 +105,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle EPERM error gracefully', async () => {
|
it('should handle EPERM error gracefully', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -137,7 +137,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle EACCES error gracefully', async () => {
|
it('should handle EACCES error gracefully', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -163,7 +163,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should propagate non-permission errors', async () => {
|
it('should propagate non-permission errors', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -186,7 +186,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not treat EPERM as graceful if syscall is not unlink', async () => {
|
it('should not treat EPERM as graceful if syscall is not unlink', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -212,7 +212,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
|
|
||||||
describe('deleteTiddler - Async/Await Mode', () => {
|
describe('deleteTiddler - Async/Await Mode', () => {
|
||||||
it('should resolve successfully without callback', async () => {
|
it('should resolve successfully without callback', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -236,7 +236,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject on non-permission errors', async () => {
|
it('should reject on non-permission errors', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -253,7 +253,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle permission errors gracefully even without callback', async () => {
|
it('should handle permission errors gracefully even without callback', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -278,7 +278,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
|
|
||||||
describe('deleteTiddler - Error Conversion', () => {
|
describe('deleteTiddler - Error Conversion', () => {
|
||||||
it('should convert string errors to Error objects', async () => {
|
it('should convert string errors to Error objects', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -300,7 +300,7 @@ describe('FileSystemAdaptor - Delete Operations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert unknown errors to Error objects', async () => {
|
it('should convert unknown errors to Error objects', async () => {
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { workspace } from '@services/wiki/wikiWorker/services';
|
import { workspace } from '@services/wiki/wikiWorker/services';
|
||||||
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
import type { IWikiWorkspace } from '@services/workspaces/interface';
|
||||||
import type { FileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ global.$tw = {
|
||||||
node: true,
|
node: true,
|
||||||
boot: {
|
boot: {
|
||||||
wikiTiddlersPath: '/test/wiki/tiddlers',
|
wikiTiddlersPath: '/test/wiki/tiddlers',
|
||||||
files: {} as Record<string, FileInfo>,
|
files: {} as Record<string, IFileInfo>,
|
||||||
},
|
},
|
||||||
utils: mockUtils,
|
utils: mockUtils,
|
||||||
};
|
};
|
||||||
|
|
@ -166,7 +166,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass existing fileInfo with overwrite flag', async () => {
|
it('should pass existing fileInfo with overwrite flag', async () => {
|
||||||
const existingFileInfo: FileInfo = {
|
const existingFileInfo: IFileInfo = {
|
||||||
filepath: '/test/old.tid',
|
filepath: '/test/old.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { workspace } from '@services/wiki/wikiWorker/services';
|
import { workspace } from '@services/wiki/wikiWorker/services';
|
||||||
import type { FileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
import type { IFileInfo, Tiddler, Wiki } from 'tiddlywiki';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
import { FileSystemAdaptor } from '../FileSystemAdaptor';
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ global.$tw = {
|
||||||
node: true,
|
node: true,
|
||||||
boot: {
|
boot: {
|
||||||
wikiTiddlersPath: '/test/wiki/tiddlers',
|
wikiTiddlersPath: '/test/wiki/tiddlers',
|
||||||
files: {} as Record<string, FileInfo>,
|
files: {} as Record<string, IFileInfo>,
|
||||||
},
|
},
|
||||||
utils: mockUtils,
|
utils: mockUtils,
|
||||||
};
|
};
|
||||||
|
|
@ -77,7 +77,7 @@ describe('FileSystemAdaptor - Save Operations', () => {
|
||||||
fields: { title: 'TestTiddler', text: 'Test content' },
|
fields: { title: 'TestTiddler', text: 'Test content' },
|
||||||
} as Tiddler;
|
} as Tiddler;
|
||||||
|
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -155,7 +155,7 @@ describe('FileSystemAdaptor - Save Operations', () => {
|
||||||
fields: { title: 'TestTiddler' },
|
fields: { title: 'TestTiddler' },
|
||||||
} as Tiddler;
|
} as Tiddler;
|
||||||
|
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -182,7 +182,7 @@ describe('FileSystemAdaptor - Save Operations', () => {
|
||||||
fields: { title: 'TestTiddler' },
|
fields: { title: 'TestTiddler' },
|
||||||
} as Tiddler;
|
} as Tiddler;
|
||||||
|
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -236,7 +236,7 @@ describe('FileSystemAdaptor - Save Operations', () => {
|
||||||
fields: { title: 'TestTiddler' },
|
fields: { title: 'TestTiddler' },
|
||||||
} as Tiddler;
|
} as Tiddler;
|
||||||
|
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -264,7 +264,7 @@ describe('FileSystemAdaptor - Save Operations', () => {
|
||||||
fields: { title: 'TestTiddler' },
|
fields: { title: 'TestTiddler' },
|
||||||
} as Tiddler;
|
} as Tiddler;
|
||||||
|
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
@ -304,7 +304,7 @@ describe('FileSystemAdaptor - Save Operations', () => {
|
||||||
fields: { title: 'TestTiddler' },
|
fields: { title: 'TestTiddler' },
|
||||||
} as Tiddler;
|
} as Tiddler;
|
||||||
|
|
||||||
const fileInfo: FileInfo = {
|
const fileInfo: IFileInfo = {
|
||||||
filepath: '/test/wiki/tiddlers/test.tid',
|
filepath: '/test/wiki/tiddlers/test.tid',
|
||||||
type: 'application/x-tiddler',
|
type: 'application/x-tiddler',
|
||||||
hasMetaFile: false,
|
hasMetaFile: false,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* High-performance tiddler comparison utilities
|
||||||
|
* Used to detect if a tiddler has actually changed to prevent unnecessary saves
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields that change automatically during save and should be excluded from comparison
|
||||||
|
*/
|
||||||
|
const EXCLUDED_FIELDS = new Set([
|
||||||
|
'modified', // Always updated on save
|
||||||
|
'revision', // Internal TiddlyWiki revision counter
|
||||||
|
'bag', // TiddlyWeb specific
|
||||||
|
'created', // May be auto-generated
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fast comparison of two tiddlers to detect real content changes
|
||||||
|
* Strategy:
|
||||||
|
* 1. Quick length check on text field (most common change)
|
||||||
|
* 2. Compare field counts
|
||||||
|
* 3. Deep compare only if needed
|
||||||
|
*
|
||||||
|
* @param oldTiddler - Existing tiddler in wiki
|
||||||
|
* @param newTiddler - New tiddler from file
|
||||||
|
* @returns true if tiddlers are meaningfully different
|
||||||
|
*/
|
||||||
|
export function hasMeaningfulChanges(
|
||||||
|
oldTiddler: Record<string, unknown>,
|
||||||
|
newTiddler: Record<string, unknown>,
|
||||||
|
): boolean {
|
||||||
|
// Fast path: Compare text field length first (most common change)
|
||||||
|
const oldText = oldTiddler.text as string | undefined;
|
||||||
|
const newText = newTiddler.text as string | undefined;
|
||||||
|
|
||||||
|
if ((oldText?.length ?? 0) !== (newText?.length ?? 0)) {
|
||||||
|
return true; // Text length changed - definitely different
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get field keys excluding auto-generated ones
|
||||||
|
const oldKeys = new Set(Object.keys(oldTiddler)).difference(EXCLUDED_FIELDS);
|
||||||
|
const newKeys = new Set(Object.keys(newTiddler)).difference(EXCLUDED_FIELDS);
|
||||||
|
|
||||||
|
// Quick check: Different number of fields
|
||||||
|
if (oldKeys.size !== newKeys.size) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are any different keys (fields added or removed)
|
||||||
|
if (oldKeys.difference(newKeys).size > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep comparison: Check each field value (keys are the same at this point)
|
||||||
|
for (const key of oldKeys) {
|
||||||
|
if (oldTiddler[key] !== newTiddler[key]) {
|
||||||
|
return true; // Field value changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // No meaningful changes
|
||||||
|
}
|
||||||
|
|
@ -38,37 +38,52 @@ export function startNodeJSWiki({
|
||||||
userName,
|
userName,
|
||||||
workspace,
|
workspace,
|
||||||
}: IStartNodeJSWikiConfigs): Observable<IWikiMessage> {
|
}: IStartNodeJSWikiConfigs): Observable<IWikiMessage> {
|
||||||
// Wait for services to be ready before using intercept with logFor
|
|
||||||
onWorkerServicesReady(() => {
|
|
||||||
void native.logFor(workspace.name, 'info', 'test-id-WorkerServicesReady');
|
|
||||||
const textDecoder = new TextDecoder();
|
|
||||||
intercept(
|
|
||||||
(newStdOut: string | Uint8Array) => {
|
|
||||||
const message = typeof newStdOut === 'string' ? newStdOut : textDecoder.decode(newStdOut);
|
|
||||||
// Send to main process logger if services are ready
|
|
||||||
void native.logFor(workspace.name, 'info', message).catch((error: unknown) => {
|
|
||||||
console.error('[intercept] Failed to send stdout to main process:', error, message, JSON.stringify(workspace));
|
|
||||||
});
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
(newStdError: string | Uint8Array) => {
|
|
||||||
const message = typeof newStdError === 'string' ? newStdError : textDecoder.decode(newStdError);
|
|
||||||
// Send to main process logger if services are ready
|
|
||||||
void native.logFor(workspace.name, 'error', message).catch((error: unknown) => {
|
|
||||||
console.error('[intercept] Failed to send stderr to main process:', error, message);
|
|
||||||
});
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (openDebugger === true) {
|
|
||||||
inspector.open();
|
|
||||||
inspector.waitForDebugger();
|
|
||||||
// eslint-disable-next-line no-debugger
|
|
||||||
debugger;
|
|
||||||
}
|
|
||||||
return new Observable<IWikiMessage>((observer) => {
|
return new Observable<IWikiMessage>((observer) => {
|
||||||
|
if (openDebugger === true) {
|
||||||
|
inspector.open();
|
||||||
|
inspector.waitForDebugger();
|
||||||
|
// eslint-disable-next-line no-debugger
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
// Wait for services to be ready before using intercept with logFor
|
||||||
|
onWorkerServicesReady(() => {
|
||||||
|
void native.logFor(workspace.name, 'info', 'test-id-WorkerServicesReady');
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
intercept(
|
||||||
|
(newStdOut: string | Uint8Array) => {
|
||||||
|
const message = typeof newStdOut === 'string' ? newStdOut : textDecoder.decode(newStdOut);
|
||||||
|
// Send to main process logger if services are ready
|
||||||
|
void native.logFor(workspace.name, 'info', message).catch((error: unknown) => {
|
||||||
|
console.error('[intercept] Failed to send stdout to main process:', error, message, JSON.stringify(workspace));
|
||||||
|
});
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
(newStdError: string | Uint8Array) => {
|
||||||
|
const message = typeof newStdError === 'string' ? newStdError : textDecoder.decode(newStdError);
|
||||||
|
// Send to main process logger if services are ready
|
||||||
|
void native.logFor(workspace.name, 'error', message).catch((error: unknown) => {
|
||||||
|
console.error('[intercept] Failed to send stderr to main process:', error, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Detect critical plugin loading errors that can cause white screen
|
||||||
|
// These errors occur during TiddlyWiki boot module execution
|
||||||
|
if (
|
||||||
|
message.includes('Error executing boot module') ||
|
||||||
|
message.includes('Cannot find module')
|
||||||
|
) {
|
||||||
|
observer.next({
|
||||||
|
type: 'control',
|
||||||
|
source: 'plugin-error',
|
||||||
|
actions: WikiControlActions.error,
|
||||||
|
message,
|
||||||
|
argv: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
let fullBootArgv: string[] = [];
|
let fullBootArgv: string[] = [];
|
||||||
// mark isDev as used to satisfy lint when not needed directly
|
// mark isDev as used to satisfy lint when not needed directly
|
||||||
void isDev;
|
void isDev;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue