fix: recover startup vault loading
This commit is contained in:
15
index.html
15
index.html
@@ -26,16 +26,8 @@
|
||||
<title>Tolaria</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="tolaria-boot-diagnostics" style="position:fixed;left:24px;top:24px;z-index:2147483647;max-width:min(760px,calc(100vw - 48px));max-height:calc(100vh - 48px);overflow:auto;margin:0;padding:12px 14px;border-radius:8px;background:#111;color:#fff;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap">Tolaria boot: HTML parsed</pre>
|
||||
<script>
|
||||
(function () {
|
||||
var diagnostics = document.getElementById('tolaria-boot-diagnostics');
|
||||
function appendDiagnostic(message) {
|
||||
if (!diagnostics) return;
|
||||
if (!diagnostics.isConnected) document.body.appendChild(diagnostics);
|
||||
diagnostics.hidden = false;
|
||||
diagnostics.textContent += '\n' + message;
|
||||
}
|
||||
function isResizeObserverLoopMessage(message) {
|
||||
return typeof message === 'string'
|
||||
&& (
|
||||
@@ -46,21 +38,14 @@
|
||||
window.addEventListener('error', function (event) {
|
||||
if (isResizeObserverLoopMessage(event.message)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
appendDiagnostic('error: ' + event.message + ' @ ' + event.filename + ':' + event.lineno + ':' + event.colno);
|
||||
});
|
||||
window.addEventListener('unhandledrejection', function (event) {
|
||||
var reason = event.reason && (event.reason.stack || event.reason.message || String(event.reason));
|
||||
if (isResizeObserverLoopMessage(reason)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
appendDiagnostic('unhandled rejection: ' + reason);
|
||||
});
|
||||
window.addEventListener('tolaria:frontend-ready', function () {
|
||||
if (diagnostics) diagnostics.hidden = true;
|
||||
}, { once: true });
|
||||
|
||||
var key = 'tolaria-theme';
|
||||
var legacyKey = 'laputa-theme';
|
||||
|
||||
94
src/hooks/useVaultLoader.empty-cache.test.ts
Normal file
94
src/hooks/useVaultLoader.empty-cache.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { useVaultLoader } from './useVaultLoader'
|
||||
|
||||
const backendInvokeFn = vi.fn()
|
||||
let mockIsTauri = true
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({
|
||||
invoke: (...args: unknown[]) => backendInvokeFn(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../mock-tauri', () => ({
|
||||
isTauri: () => mockIsTauri,
|
||||
mockInvoke: (command: string, args?: Record<string, unknown>) => backendInvokeFn(command, args),
|
||||
}))
|
||||
|
||||
function makeEntry(path: string, title: string): VaultEntry {
|
||||
return {
|
||||
path,
|
||||
filename: path.split('/').pop() ?? 'note.md',
|
||||
title,
|
||||
isA: 'Note',
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
status: 'Active',
|
||||
archived: false,
|
||||
modifiedAt: 1,
|
||||
createdAt: 1,
|
||||
fileSize: 100,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
sidebarLabel: null,
|
||||
template: null,
|
||||
sort: null,
|
||||
view: null,
|
||||
visible: null,
|
||||
organized: false,
|
||||
favorite: false,
|
||||
favoriteIndex: null,
|
||||
listPropertiesDisplay: [],
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
hasH1: true,
|
||||
fileKind: 'markdown',
|
||||
}
|
||||
}
|
||||
|
||||
function commandKey(command: string, args?: Record<string, unknown>): string {
|
||||
return `${command}:${typeof args?.path === 'string' ? args.path : ''}`
|
||||
}
|
||||
|
||||
describe('useVaultLoader empty cache recovery', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsTauri = true
|
||||
})
|
||||
|
||||
it('reloads a background workspace when its cached startup listing is empty in Tauri mode', async () => {
|
||||
const brian = { label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: true }
|
||||
const laputa = { label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true }
|
||||
const commandResults = new Map<string, unknown>([
|
||||
['reload_vault:/brian', [makeEntry('/brian/note/hello.md', 'Brian Hello')]],
|
||||
['list_vault:/laputa', []],
|
||||
['reload_vault:/laputa', [makeEntry('/laputa/note/hello.md', 'Laputa Hello')]],
|
||||
['get_modified_files:', []],
|
||||
['list_vault_folders:', []],
|
||||
['list_views:', []],
|
||||
])
|
||||
|
||||
backendInvokeFn.mockImplementation((command: string, args?: Record<string, unknown>) => {
|
||||
return Promise.resolve(commandResults.get(commandKey(command, args)) ?? null)
|
||||
})
|
||||
|
||||
const vaults = [brian, laputa]
|
||||
const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.entries.map((entry) => entry.title).sort()).toEqual(['Brian Hello', 'Laputa Hello'])
|
||||
})
|
||||
|
||||
const laputaLoads = backendInvokeFn.mock.calls
|
||||
.filter(([command, args]) => {
|
||||
return args?.path === '/laputa' && (command === 'list_vault' || command === 'reload_vault')
|
||||
})
|
||||
.map(([command]) => command)
|
||||
expect(laputaLoads).toEqual(['list_vault', 'reload_vault'])
|
||||
})
|
||||
})
|
||||
@@ -931,7 +931,7 @@ function loadMissingWorkspaceEntries({
|
||||
vaultPath: string
|
||||
vaults?: VaultOption[]
|
||||
}) {
|
||||
void loadWorkspaceEntries(vault, defaultWorkspacePath)
|
||||
void loadWorkspaceEntries(vault, defaultWorkspacePath, { reloadIfEmpty: true })
|
||||
.then((loadedEntries) => {
|
||||
if (!isCurrentVaultPath(vaultPath)) return
|
||||
loadedPaths.add(vault.path)
|
||||
|
||||
@@ -24,6 +24,11 @@ interface MountedVaultEntriesOptions extends VaultPathOptions {
|
||||
|
||||
type MountedVaultFoldersOptions = MountedVaultEntriesOptions
|
||||
|
||||
interface WorkspaceEntryLoadOptions {
|
||||
forceReload?: boolean
|
||||
reloadIfEmpty?: boolean
|
||||
}
|
||||
|
||||
interface CommitWithPushOptions extends VaultPathOptions {
|
||||
message: string
|
||||
}
|
||||
@@ -63,24 +68,53 @@ function loadVaultEntriesWithCommand({ vaultPath, command }: VaultPathOptions &
|
||||
.then((entries) => normalizeVaultEntries(entries, vaultPath))
|
||||
}
|
||||
|
||||
function shouldSkipMountedVault(vault: VaultOption): boolean {
|
||||
return vault.available === false || vault.mounted === false || !vault.path.trim()
|
||||
}
|
||||
|
||||
function shouldReloadEmptyWorkspaceResult(entries: VaultEntry[], options: WorkspaceEntryLoadOptions): boolean {
|
||||
return entries.length === 0 && options.reloadIfEmpty === true && !options.forceReload && isTauri()
|
||||
}
|
||||
|
||||
function shouldIncludeFallbackVault(
|
||||
byPath: Map<string, VaultOption>,
|
||||
vaultPath: string,
|
||||
includeFallbackVault: boolean,
|
||||
): boolean {
|
||||
if (!includeFallbackVault) return false
|
||||
if (!vaultPath.trim()) return false
|
||||
return !byPath.has(vaultPath)
|
||||
}
|
||||
|
||||
function loadWorkspaceEntriesWithCommand(
|
||||
vault: VaultOption,
|
||||
command: 'list_vault' | 'reload_vault',
|
||||
defaultWorkspacePath?: string | null,
|
||||
): Promise<VaultEntry[]> {
|
||||
const workspace = workspaceIdentityFromVault(vault, { defaultWorkspacePath })
|
||||
return tauriCall<unknown>({ command, tauriArgs: { path: vault.path } })
|
||||
.then((entries) => normalizeVaultEntries(entries, vault.path, workspace))
|
||||
}
|
||||
|
||||
export function loadWorkspaceEntries(
|
||||
vault: VaultOption,
|
||||
defaultWorkspacePath?: string | null,
|
||||
options: { forceReload?: boolean } = {},
|
||||
options: WorkspaceEntryLoadOptions = {},
|
||||
): Promise<VaultEntry[]> {
|
||||
const workspace = workspaceIdentityFromVault(vault, { defaultWorkspacePath })
|
||||
const command = options.forceReload && isTauri() ? 'reload_vault' : 'list_vault'
|
||||
return tauriCall<unknown>({ command, tauriArgs: { path: vault.path } })
|
||||
.then((entries) => normalizeVaultEntries(entries, vault.path, workspace))
|
||||
return loadWorkspaceEntriesWithCommand(vault, command, defaultWorkspacePath)
|
||||
.then((entries) => shouldReloadEmptyWorkspaceResult(entries, options)
|
||||
? loadWorkspaceEntriesWithCommand(vault, 'reload_vault', defaultWorkspacePath)
|
||||
: entries)
|
||||
}
|
||||
|
||||
function uniqueMountedVaults({ vaultPath, vaults = [], includeFallbackVault = true }: MountedVaultEntriesOptions): VaultOption[] {
|
||||
const byPath = new Map<string, VaultOption>()
|
||||
for (const vault of vaults) {
|
||||
if (vault.available === false || vault.mounted === false || !vault.path.trim()) continue
|
||||
if (shouldSkipMountedVault(vault)) continue
|
||||
byPath.set(vault.path, vault)
|
||||
}
|
||||
if (includeFallbackVault && vaultPath.trim() && !byPath.has(vaultPath)) {
|
||||
if (shouldIncludeFallbackVault(byPath, vaultPath, includeFallbackVault)) {
|
||||
byPath.set(vaultPath, { label: vaultPath.split('/').filter(Boolean).pop() || 'Workspace', path: vaultPath, mounted: true, available: true })
|
||||
}
|
||||
return [...byPath.values()]
|
||||
|
||||
@@ -4,13 +4,13 @@ import { describe, expect, it } from 'vitest'
|
||||
function firstInlineScriptFromIndex(): string {
|
||||
const indexHtml = readFileSync(`${process.cwd()}/index.html`, 'utf8')
|
||||
const match = indexHtml.match(/<script>\s*([\s\S]*?)\s*<\/script>/)
|
||||
if (!match) throw new Error('index.html boot diagnostics script was not found')
|
||||
if (!match) throw new Error('index.html startup script was not found')
|
||||
return match[1]
|
||||
}
|
||||
|
||||
describe('index boot diagnostics', () => {
|
||||
describe('index startup script', () => {
|
||||
it('does not show the boot overlay for ResizeObserver loop notifications', () => {
|
||||
document.body.innerHTML = '<pre id="tolaria-boot-diagnostics">Tolaria boot: HTML parsed</pre>'
|
||||
document.body.innerHTML = ''
|
||||
new Function(firstInlineScriptFromIndex())()
|
||||
|
||||
const event = new ErrorEvent('error', {
|
||||
@@ -20,11 +20,11 @@ describe('index boot diagnostics', () => {
|
||||
window.dispatchEvent(event)
|
||||
|
||||
expect(event.defaultPrevented).toBe(true)
|
||||
expect(document.getElementById('tolaria-boot-diagnostics')?.textContent).toBe('Tolaria boot: HTML parsed')
|
||||
expect(document.body.children).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('still shows the boot overlay for real startup errors', () => {
|
||||
document.body.innerHTML = '<pre id="tolaria-boot-diagnostics">Tolaria boot: HTML parsed</pre>'
|
||||
it('does not create a visible boot overlay for real startup errors', () => {
|
||||
document.body.innerHTML = ''
|
||||
new Function(firstInlineScriptFromIndex())()
|
||||
|
||||
window.dispatchEvent(new ErrorEvent('error', {
|
||||
@@ -34,8 +34,6 @@ describe('index boot diagnostics', () => {
|
||||
colno: 2,
|
||||
}))
|
||||
|
||||
expect(document.getElementById('tolaria-boot-diagnostics')?.textContent).toContain(
|
||||
'error: startup failed @ app.js:1:2',
|
||||
)
|
||||
expect(document.body.children).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user