mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
type PromiseSettlement<T> =
|
|
| { status: 'fulfilled'; value: T }
|
|
| { status: 'rejected'; error: unknown }
|
|
|
|
type PromiseSettlementWaiter<T> = {
|
|
resolve: (value: T) => void
|
|
reject: (error: unknown) => void
|
|
onFulfilled?: (value: T) => void
|
|
signal?: AbortSignal
|
|
onAbort: () => void
|
|
timer?: ReturnType<typeof setTimeout>
|
|
}
|
|
|
|
export type PromiseSettlementWaitOptions<T> = {
|
|
signal?: AbortSignal
|
|
/** Preserve Promise.race ordering when raw settlement and abort share a turn. */
|
|
abortInMicrotask?: boolean
|
|
timeoutMs?: number
|
|
createAbortError?: () => unknown
|
|
createTimeoutError?: () => Error
|
|
onFulfilled?: (value: T) => void
|
|
onAbandon?: (reason: 'abort' | 'timeout') => void
|
|
}
|
|
|
|
// Why: Promise.then reactions cannot be detached. One reaction fans settlement
|
|
// into explicit waiters so aborted or timed-out callers release their closures.
|
|
export class PromiseSettlementWaiters<T> {
|
|
private readonly waiters = new Set<PromiseSettlementWaiter<T>>()
|
|
private settlement: PromiseSettlement<T> | null = null
|
|
|
|
constructor(
|
|
readonly promise: Promise<T>,
|
|
onSettled?: () => void
|
|
) {
|
|
void this.promise.then(
|
|
(value) => this.settle({ status: 'fulfilled', value }, onSettled),
|
|
(error: unknown) => this.settle({ status: 'rejected', error }, onSettled)
|
|
)
|
|
}
|
|
|
|
get waiterCount(): number {
|
|
return this.waiters.size
|
|
}
|
|
|
|
wait(options: PromiseSettlementWaitOptions<T> = {}): Promise<T> {
|
|
if (options.signal?.aborted) {
|
|
options.onAbandon?.('abort')
|
|
return Promise.reject(options.createAbortError?.() ?? createDefaultAbortError())
|
|
}
|
|
if (this.settlement) {
|
|
return settleWaiterImmediately(this.settlement, options.onFulfilled)
|
|
}
|
|
return new Promise<T>((resolve, reject) => {
|
|
let waiter!: PromiseSettlementWaiter<T>
|
|
const abandon = (reason: 'abort' | 'timeout', error: unknown): void => {
|
|
if (!this.waiters.delete(waiter)) {
|
|
return
|
|
}
|
|
cleanupWaiter(waiter)
|
|
options.onAbandon?.(reason)
|
|
reject(error)
|
|
}
|
|
const onAbort = (): void => {
|
|
const error = options.createAbortError?.() ?? createDefaultAbortError()
|
|
if (options.abortInMicrotask) {
|
|
queueMicrotask(() => abandon('abort', error))
|
|
} else {
|
|
abandon('abort', error)
|
|
}
|
|
}
|
|
waiter = {
|
|
resolve,
|
|
reject,
|
|
onFulfilled: options.onFulfilled,
|
|
signal: options.signal,
|
|
onAbort
|
|
}
|
|
if (options.timeoutMs !== undefined) {
|
|
waiter.timer = setTimeout(
|
|
() =>
|
|
abandon(
|
|
'timeout',
|
|
options.createTimeoutError?.() ?? new Error('Promise settlement wait timed out')
|
|
),
|
|
Math.max(1, options.timeoutMs)
|
|
)
|
|
waiter.timer.unref?.()
|
|
}
|
|
this.waiters.add(waiter)
|
|
options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
if (options.signal?.aborted) {
|
|
onAbort()
|
|
}
|
|
})
|
|
}
|
|
|
|
private settle(settlement: PromiseSettlement<T>, onSettled?: () => void): void {
|
|
if (this.settlement) {
|
|
return
|
|
}
|
|
this.settlement = settlement
|
|
for (const waiter of this.waiters) {
|
|
this.waiters.delete(waiter)
|
|
cleanupWaiter(waiter)
|
|
settleWaiter(settlement, waiter)
|
|
}
|
|
onSettled?.()
|
|
}
|
|
}
|
|
|
|
function cleanupWaiter<T>(waiter: PromiseSettlementWaiter<T>): void {
|
|
if (waiter.timer) {
|
|
clearTimeout(waiter.timer)
|
|
}
|
|
waiter.signal?.removeEventListener('abort', waiter.onAbort)
|
|
}
|
|
|
|
function settleWaiter<T>(
|
|
settlement: PromiseSettlement<T>,
|
|
waiter: Pick<PromiseSettlementWaiter<T>, 'resolve' | 'reject' | 'onFulfilled'>
|
|
): void {
|
|
if (settlement.status === 'rejected') {
|
|
waiter.reject(settlement.error)
|
|
return
|
|
}
|
|
try {
|
|
waiter.onFulfilled?.(settlement.value)
|
|
waiter.resolve(settlement.value)
|
|
} catch (error) {
|
|
waiter.reject(error)
|
|
}
|
|
}
|
|
|
|
function settleWaiterImmediately<T>(
|
|
settlement: PromiseSettlement<T>,
|
|
onFulfilled?: (value: T) => void
|
|
): Promise<T> {
|
|
return new Promise((resolve, reject) =>
|
|
settleWaiter(settlement, { resolve, reject, onFulfilled })
|
|
)
|
|
}
|
|
|
|
function createDefaultAbortError(): Error {
|
|
const error = new Error('Promise settlement wait aborted')
|
|
error.name = 'AbortError'
|
|
return error
|
|
}
|