refactor(core): decouple run settling from terminal status transition
Resolve #running before the terminal statuschange so the settle signal can never be lost to re-entrant listeners. Hooks keep middleware semantics: a throwing hook fails the run; integrations that don't want this should suppress errors in their own hooks. Also make suppress() async-aware so rejected promises (e.g. showMask) are actually caught.
This commit is contained in:
@@ -194,7 +194,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the current task and wait until the run has fully settled.
|
* Stop the current task and wait until the run has fully settled (including lifecycle hooks).
|
||||||
|
* @note never await .stop() in a lifecycle hook.
|
||||||
*/
|
*/
|
||||||
async stop(): Promise<void> {
|
async stop(): Promise<void> {
|
||||||
if (this.#status !== 'running') return
|
if (this.#status !== 'running') return
|
||||||
@@ -223,14 +224,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
this.#setStatus('running')
|
this.#setStatus('running')
|
||||||
this.#emitHistoryChange()
|
this.#emitHistoryChange()
|
||||||
|
|
||||||
let settleRun!: (status: AgentStatus) => void
|
let resolveRunning!: () => void
|
||||||
this.#running = new Promise<void>(
|
this.#running = new Promise<void>((r) => (resolveRunning = r))
|
||||||
(resolve) =>
|
|
||||||
(settleRun = (status) => {
|
|
||||||
this.#setStatus(status)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Disable ask_user tool if onAskUser is not set
|
// Disable ask_user tool if onAskUser is not set
|
||||||
if (!this.onAskUser) this.tools.delete('ask_user')
|
if (!this.onAskUser) this.tools.delete('ask_user')
|
||||||
@@ -257,8 +252,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
try {
|
try {
|
||||||
console.group(`step: ${step}`)
|
console.group(`step: ${step}`)
|
||||||
|
|
||||||
// Abort may have fired between steps (e.g. during stepDelay).
|
// inside the try: abort between steps must settle as `stopped`
|
||||||
// Check inside the try so it is classified as `stopped`, and skip the observe phase.
|
|
||||||
this.#abortController.signal.throwIfAborted()
|
this.#abortController.signal.throwIfAborted()
|
||||||
|
|
||||||
// observe
|
// observe
|
||||||
@@ -371,7 +365,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
suppress(() => this.pageController.cleanUpHighlights())
|
suppress(() => this.pageController.cleanUpHighlights())
|
||||||
suppress(() => this.pageController.hideMask())
|
suppress(() => this.pageController.hideMask())
|
||||||
this.#abortController.abort()
|
this.#abortController.abort()
|
||||||
settleRun(finalStatus)
|
resolveRunning()
|
||||||
|
this.#setStatus(finalStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,9 +133,9 @@ export function assert(condition: unknown, message?: string, silent?: boolean):
|
|||||||
/**
|
/**
|
||||||
* Suppress errors from a function.
|
* Suppress errors from a function.
|
||||||
*/
|
*/
|
||||||
export function suppress<T>(fn: () => T): T | undefined {
|
export async function suppress<T>(fn: () => T | Promise<T>): Promise<Awaited<T> | undefined> {
|
||||||
try {
|
try {
|
||||||
return fn()
|
return await fn()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user