Merge PR #1606: isolate workspace DOM during rapid navigation
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -34,6 +34,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
### Fixed
|
||||
- The dubbing editor's video and transcript columns can now be resized by pointer or keyboard, and the chosen split persists across launches (#1571) — thanks @invio-a11y!
|
||||
- CPU-only synthesis now gets a bounded ten-minute execution budget, and a render that exhausts it is reported as a compute timeout instead of misleading "generation capacity is busy" queue pressure (#1588) — thanks @ChienNguyen1111!
|
||||
- Rapid Launchpad ↔ Dub navigation now replaces the workspace DOM owner cleanly, so late media/waveform cleanup cannot trigger React's `insertBefore` crash (#1590) — thanks @nicolas-jacques!
|
||||
- Watermark embedding failures now log the full traceback instead of just the exception message, so a silently-unmarked-audio incident (audio passes through unmarked by design) is diagnosable from the log alone (#1576) — thanks @paoloantinori!
|
||||
- Dubbing now recovers rapid two-speaker exchanges when diarization collapses them, defaults new projects to lip sync without overwriting saved timing choices, and keeps the editor usable on narrow screens (#1584) — thanks @victordonat0!
|
||||
- `OMNIVOICE_ASR_BACKEND=omnivoice` now selects the PyTorch-native Whisper path, so the documented ROCm escape hatch no longer fails as an unknown engine (#1582) — thanks @patmansk!
|
||||
|
||||
@@ -812,6 +812,15 @@ VoiceStudio now tries, in order: the default GitHub host → a gh-proxy mirror
|
||||
|
||||
**Linked issues:** [#130](https://github.com/debpalash/VoiceStudio/issues/130), [#60](https://github.com/debpalash/VoiceStudio/issues/60), [#57](https://github.com/debpalash/VoiceStudio/issues/57)
|
||||
|
||||
## Workspace navigation crashes with `insertBefore` / `NotFoundError`
|
||||
|
||||
This was a `v0.5.0` workspace-lifecycle bug exposed by rapid Launchpad ↔ Dub
|
||||
navigation while media renderers were cleaning up. Current builds isolate each
|
||||
workspace under its own DOM owner. Update VoiceStudio; no model or project data
|
||||
repair is required.
|
||||
|
||||
**Linked issue:** [#1590](https://github.com/debpalash/VoiceStudio/issues/1590)
|
||||
|
||||
## Uninstalling / removing all of VoiceStudio's data
|
||||
|
||||
VoiceStudio is fully local — no accounts, no services, nothing to deactivate. To
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAppStore, FONT_STACKS } from './store';
|
||||
import { NAV_ITEMS } from './components/navItems';
|
||||
import SearchableSelect from './components/SearchableSelect';
|
||||
import DirectionDialog from './components/DirectionDialog';
|
||||
import ModeLifecycleBoundary from './components/ModeLifecycleBoundary';
|
||||
import { resolveDubDefaultTrack } from './utils/dubDefaultTrack';
|
||||
|
||||
// Lazy-load heavy/conditional components so they don't bloat the initial bundle.
|
||||
@@ -1451,7 +1452,7 @@ function App() {
|
||||
<NavRail mode={mode} setMode={setMode} side={navRailSide} onFlipSide={flipNavRailSide} />
|
||||
)}
|
||||
|
||||
<div className="main-content">
|
||||
<ModeLifecycleBoundary mode={mode}>
|
||||
{/* ═══ LAUNCHPAD TAB ═══ */}
|
||||
{mode === 'settings' ? (
|
||||
<ErrorBoundary name="settings">
|
||||
@@ -1779,7 +1780,7 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModeLifecycleBoundary>
|
||||
|
||||
{/* ── SIDEBAR ── */}
|
||||
<Suspense fallback={<LazyFallback />}>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Owns the DOM subtree for one top-level workspace.
|
||||
*
|
||||
* Some workspaces host imperative renderers (WaveSurfer, media elements, and
|
||||
* portals). Replacing the host when navigation changes prevents a late
|
||||
* renderer cleanup from mutating the next workspace's React-owned DOM.
|
||||
*/
|
||||
export default function ModeLifecycleBoundary({ mode, children }) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div key={mode} className="main-content" data-mode={mode}>
|
||||
{children}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
import { Fragment } from 'react';
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import ModeLifecycleBoundary from './ModeLifecycleBoundary';
|
||||
|
||||
function ImperativeWorkspace({ name }) {
|
||||
const hostRef = useRef(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const host = hostRef.current;
|
||||
const owned = document.createElement('div');
|
||||
owned.dataset.owner = name;
|
||||
host.appendChild(owned);
|
||||
return () => {
|
||||
// Model a renderer whose asynchronous destroy completes after React has
|
||||
// committed the next route. If the DOM host were reused, this would
|
||||
// erase that route's children and recreate the reported ownership race.
|
||||
setTimeout(() => host.replaceChildren(), 0);
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
return <section ref={hostRef}>{name}</section>;
|
||||
}
|
||||
|
||||
describe('ModeLifecycleBoundary', () => {
|
||||
it('replaces the DOM owner throughout the reported rapid navigation loop', () => {
|
||||
vi.useFakeTimers();
|
||||
const sequence = ['launchpad', 'dub', 'dub', 'launchpad', 'dub'];
|
||||
const view = render(
|
||||
<ModeLifecycleBoundary mode={sequence[0]}>
|
||||
<ImperativeWorkspace name={sequence[0]} />
|
||||
</ModeLifecycleBoundary>,
|
||||
);
|
||||
let previousHost = view.container.firstElementChild;
|
||||
let previousMode = sequence[0];
|
||||
|
||||
for (const mode of sequence.slice(1)) {
|
||||
view.rerender(
|
||||
<ModeLifecycleBoundary mode={mode}>
|
||||
<ImperativeWorkspace name={mode} />
|
||||
</ModeLifecycleBoundary>,
|
||||
);
|
||||
const nextHost = view.container.firstElementChild;
|
||||
if (mode !== previousMode) {
|
||||
expect(nextHost).not.toBe(previousHost);
|
||||
expect(previousHost.isConnected).toBe(false);
|
||||
} else {
|
||||
expect(nextHost).toBe(previousHost);
|
||||
}
|
||||
expect(nextHost.querySelector('[data-owner]')?.dataset.owner).toBe(mode);
|
||||
vi.runAllTimers();
|
||||
expect(nextHost.textContent).toContain(mode);
|
||||
expect(nextHost.querySelector('[data-owner]')?.dataset.owner).toBe(mode);
|
||||
previousHost = nextHost;
|
||||
previousMode = mode;
|
||||
}
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user