mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 08:02:38 +00:00
Fix Monaco Delayer cancellation during renderer teardown
Handle expected Monaco Delayer cancellation during renderer cleanup so ignored trigger promises do not surface as unhandled rejections. Adds regression coverage for the DisposableStore teardown path from the crash report.
This commit is contained in:
Vendored
+21
@@ -36,6 +36,27 @@ declare module 'monaco-editor/esm/vs/editor/browser/controller/editContext/clipb
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'monaco-editor/esm/vs/base/common/async.js' {
|
||||
export class Delayer<T = unknown> {
|
||||
constructor(defaultDelay: number)
|
||||
trigger(task: () => T | Promise<T>, delay?: number): Promise<T | undefined>
|
||||
cancel(): void
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'monaco-editor/esm/vs/base/common/lifecycle.js' {
|
||||
type Disposable = {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export class DisposableStore {
|
||||
add<T extends Disposable>(disposable: T): T
|
||||
clear(): void
|
||||
dispose(): void
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
var MonacoEnvironment:
|
||||
| {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Delayer } from 'monaco-editor/esm/vs/base/common/async.js'
|
||||
import { DisposableStore } from 'monaco-editor/esm/vs/base/common/lifecycle.js'
|
||||
import { installMonacoDelayerCancellationGuard } from './monaco-delayer-cancellation-guard'
|
||||
|
||||
const UNHANDLED_REJECTION_SETTLE_MS = 20
|
||||
|
||||
async function collectUnhandledRejections(run: () => void): Promise<unknown[]> {
|
||||
const reasons: unknown[] = []
|
||||
const onUnhandledRejection = (reason: unknown): void => {
|
||||
reasons.push(reason)
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', onUnhandledRejection)
|
||||
try {
|
||||
run()
|
||||
await new Promise((resolve) => setTimeout(resolve, UNHANDLED_REJECTION_SETTLE_MS))
|
||||
} finally {
|
||||
process.off('unhandledRejection', onUnhandledRejection)
|
||||
}
|
||||
|
||||
return reasons
|
||||
}
|
||||
|
||||
describe('installMonacoDelayerCancellationGuard', () => {
|
||||
it('marks DisposableStore Delayer cancellation as handled when the trigger promise is ignored', async () => {
|
||||
installMonacoDelayerCancellationGuard()
|
||||
installMonacoDelayerCancellationGuard()
|
||||
|
||||
const unhandledRejections = await collectUnhandledRejections(() => {
|
||||
const store = new DisposableStore()
|
||||
const delayer = store.add(new Delayer(1000))
|
||||
delayer.trigger(() => undefined)
|
||||
|
||||
store.dispose()
|
||||
})
|
||||
|
||||
expect(unhandledRejections).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps cancellation visible to callers that await the trigger promise', async () => {
|
||||
installMonacoDelayerCancellationGuard()
|
||||
|
||||
const delayer = new Delayer(1000)
|
||||
const promise = delayer.trigger(() => undefined)
|
||||
delayer.cancel()
|
||||
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
name: 'Canceled',
|
||||
message: 'Canceled'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Delayer } from 'monaco-editor/esm/vs/base/common/async.js'
|
||||
|
||||
const MONACO_CANCELLATION_NAME = 'Canceled'
|
||||
|
||||
type MonacoDelayerInstance = {
|
||||
cancel: () => void
|
||||
completionPromise?: Promise<unknown> | null
|
||||
}
|
||||
|
||||
type GuardedDelayerPrototype = MonacoDelayerInstance & {
|
||||
__orcaDelayerCancellationGuardInstalled?: true
|
||||
}
|
||||
|
||||
function isMonacoCancellationError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.name === MONACO_CANCELLATION_NAME &&
|
||||
error.message === MONACO_CANCELLATION_NAME
|
||||
)
|
||||
}
|
||||
|
||||
export function installMonacoDelayerCancellationGuard(): void {
|
||||
const delayerPrototype = Delayer.prototype as GuardedDelayerPrototype
|
||||
if (delayerPrototype.__orcaDelayerCancellationGuardInstalled) {
|
||||
return
|
||||
}
|
||||
|
||||
const originalCancel = delayerPrototype.cancel
|
||||
delayerPrototype.cancel = function cancelWithHandledCancellation(this: MonacoDelayerInstance) {
|
||||
const completionPromise = this.completionPromise
|
||||
if (completionPromise) {
|
||||
// Why: Monaco Delayer cancellation is normal during DisposableStore
|
||||
// teardown, but ignored trigger promises surface as unhandled rejections.
|
||||
void completionPromise.catch((error) => {
|
||||
if (!isMonacoCancellationError(error)) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
originalCancel.call(this)
|
||||
}
|
||||
delayerPrototype.__orcaDelayerCancellationGuardInstalled = true
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { registerAstroLanguage } from './monaco-languages/register-astro'
|
||||
import { registerNimLanguage } from './monaco-languages/register-nim'
|
||||
import { registerSvelteLanguage } from './monaco-languages/register-svelte'
|
||||
import { registerVueLanguage } from './monaco-languages/register-vue'
|
||||
import { installMonacoDelayerCancellationGuard } from './monaco-delayer-cancellation-guard'
|
||||
import { installMonacoDiffEditorDisposalGuard } from './monaco-diff-editor-disposal'
|
||||
import { installMonacoContextMenuPaste } from '@/components/editor/install-monaco-context-menu-paste'
|
||||
|
||||
@@ -75,6 +76,7 @@ registerVueLanguage(monaco)
|
||||
registerSvelteLanguage(monaco)
|
||||
registerAstroLanguage(monaco)
|
||||
registerNimLanguage(monaco)
|
||||
installMonacoDelayerCancellationGuard()
|
||||
installMonacoDiffEditorDisposalGuard(monaco)
|
||||
// Why: Monaco's built-in context-menu Paste reads navigator.clipboard, which is
|
||||
// blocked in Orca's sandboxed renderer. Route it through the trusted IPC bridge
|
||||
|
||||
Reference in New Issue
Block a user