mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
fix(studio): plumb Request.signal through toWebHandler as req.on('close'|'aborted')
Pages-router handlers that stream (e.g. /api/ai/sql/generate-v4)
wire client-abort by subscribing to Node IncomingMessage events:
const abortController = new AbortController()
req.on('close', () => abortController.abort())
req.on('aborted', () => abortController.abort())
The toWebHandler shim built a plain NextApiRequest-shaped object with
no EventEmitter surface, so those calls crashed at runtime with
'req.on is not a function' on every request.
Surface a minimal EventEmitter (on/off/once/emit/removeListener/
removeAllListeners) that only honours the two events studio actually
uses ('close', 'aborted'), both wired to request.signal's abort
event. Other event names are accepted but never fire - good enough
for the handlers we forward through this shim.
This commit is contained in:
1 parent
8c51b7f2fa
commit
df2a806dab
1 file changed
+57
@@ -93,6 +93,32 @@ async function buildRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// Surface a tiny subset of Node's `IncomingMessage` EventEmitter API so
|
||||
// handlers that wire client-abort via `req.on('close', …)` /
|
||||
// `req.on('aborted', …)` (e.g. AI streaming routes) can still hook into
|
||||
// the Web `Request.signal`. We don't model arbitrary event types — only
|
||||
// `close` and `aborted`, both of which fire when the signal aborts.
|
||||
const listeners: Record<string, Set<(...args: unknown[]) => void>> = {}
|
||||
const fireAbort = () => {
|
||||
for (const event of ['close', 'aborted'] as const) {
|
||||
const set = listeners[event]
|
||||
if (!set) continue
|
||||
for (const fn of set) {
|
||||
try {
|
||||
fn()
|
||||
} catch {
|
||||
// Swallow — Node would emit `error` on the request, but we
|
||||
// have nothing meaningful to do with it here.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (request.signal.aborted) {
|
||||
queueMicrotask(fireAbort)
|
||||
} else {
|
||||
request.signal.addEventListener('abort', fireAbort, { once: true })
|
||||
}
|
||||
|
||||
return {
|
||||
method,
|
||||
url: url.pathname + url.search,
|
||||
@@ -100,6 +126,37 @@ async function buildRequest(
|
||||
query,
|
||||
body,
|
||||
cookies,
|
||||
on(event: string, fn: (...args: unknown[]) => void) {
|
||||
;(listeners[event] ??= new Set()).add(fn)
|
||||
return this
|
||||
},
|
||||
off(event: string, fn: (...args: unknown[]) => void) {
|
||||
listeners[event]?.delete(fn)
|
||||
return this
|
||||
},
|
||||
once(event: string, fn: (...args: unknown[]) => void) {
|
||||
const wrapper = (...args: unknown[]) => {
|
||||
listeners[event]?.delete(wrapper)
|
||||
fn(...args)
|
||||
}
|
||||
;(listeners[event] ??= new Set()).add(wrapper)
|
||||
return this
|
||||
},
|
||||
removeListener(event: string, fn: (...args: unknown[]) => void) {
|
||||
listeners[event]?.delete(fn)
|
||||
return this
|
||||
},
|
||||
removeAllListeners(event?: string) {
|
||||
if (event) listeners[event]?.clear()
|
||||
else for (const set of Object.values(listeners)) set.clear()
|
||||
return this
|
||||
},
|
||||
emit(event: string, ...args: unknown[]) {
|
||||
const set = listeners[event]
|
||||
if (!set) return false
|
||||
for (const fn of set) fn(...args)
|
||||
return true
|
||||
},
|
||||
} as unknown as NextApiRequest
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user