@christianirack/brainllm-runtime 0.9.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ // Optional public SSR cache. Standalone Redis only; no sessions, locks, socket fanout or cluster coordination.
2
+ // RESP2, one bounded operation per owned connection, no pending queue, no retries and no provider dependencies.
3
+ import net from 'node:net'
4
+ import tls from 'node:tls'
5
+ import dns from 'node:dns'
6
+ import { validateHeaderValue } from 'node:http'
7
+ import { createHash } from 'node:crypto'
8
+
9
+ const ENTRY_BYTES = 1024 * 1024, MAX_ACTIVE = 4, DEADLINE_MS = 250, MAX_TTL_MS = 86400000
10
+ const digest = value => createHash('sha256').update(value).digest('hex')
11
+ const SAFE_REQUEST = new Set(['host', 'connection', 'accept', 'accept-encoding', 'accept-language', 'user-agent', 'if-none-match', 'if-modified-since', 'cache-control', 'pragma', 'upgrade-insecure-requests', 'sec-fetch-site', 'sec-fetch-mode', 'sec-fetch-dest', 'sec-fetch-user', 'sec-ch-ua', 'sec-ch-ua-mobile', 'sec-ch-ua-platform'])
12
+ const SAFE_RESPONSE = new Set(['content-type', 'content-language', 'cache-control', 'link', 'x-robots-tag', 'cross-origin-opener-policy', 'content-security-policy'])
13
+ const privateControl = value => /(?:^|,)\s*(?:private|no-store|no-cache)(?:\s|=|,|$)/i.test(String(value || ''))
14
+
15
+ /** Unknown application headers can carry identity. They bypass rather than becoming cache-key material. */
16
+ export function publicCacheRequest(req) {
17
+ if (!req || !['GET', 'HEAD'].includes(req.method) || !req.headers || privateControl(req.headers['cache-control']) || /no-cache/i.test(req.headers.pragma || '')) return false
18
+ for (const key of Object.keys(req.headers)) if (!SAFE_REQUEST.has(key.toLowerCase())) return false
19
+ return true
20
+ }
21
+ function settings(raw, namespace, build, base) {
22
+ if (typeof namespace !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:_-]{0,63}$/.test(namespace) || typeof build !== 'string' || !build || build.length > 256 || typeof base !== 'string' || base.length > 2048 || typeof raw !== 'string' || raw.length > 4096) return null
23
+ try {
24
+ const url = new URL(raw)
25
+ if (!['redis:', 'rediss:'].includes(url.protocol) || !url.hostname || url.search || url.hash || !/^\/(?:\d{1,4})?$/.test(url.pathname || '/') || Number(url.pathname.slice(1)) > 1023) return null
26
+ const user = decodeURIComponent(url.username), password = decodeURIComponent(url.password)
27
+ if (Buffer.byteLength(user) > 128 || Buffer.byteLength(password) > 1024 || user && !password) return null
28
+ if (url.port && Number(url.port) < 1) return null
29
+ const host = url.hostname.replace(/^\[|\]$/g, '')
30
+ return { host, port: Number(url.port || 6379), secure: url.protocol === 'rediss:', user, password, db: Number(url.pathname.slice(1) || 0), prefix: `brain:ssr:v1:${digest(namespace)}:${digest(JSON.stringify([build, base]))}:` }
31
+ } catch { return null }
32
+ }
33
+ function frame(parts) {
34
+ const chunks = [Buffer.from(`*${parts.length}\r\n`)]
35
+ for (const part of parts) { const bytes = Buffer.isBuffer(part) ? part : Buffer.from(String(part)); chunks.push(Buffer.from(`$${bytes.length}\r\n`), bytes, Buffer.from('\r\n')) }
36
+ return Buffer.concat(chunks)
37
+ }
38
+ // Only replies required by AUTH/SELECT/GET/SET/DEL. Unexpected aggregates/protocol types are a miss.
39
+ function reply(buffer) {
40
+ const end = buffer.indexOf('\r\n')
41
+ if (end < 0) return buffer.length > 1024 ? false : undefined
42
+ if (end > 1024) return false
43
+ // latin1 preserves every high bit; ascii decoding masks malformed bytes into valid digits or OK.
44
+ const type = buffer[0], text = buffer.toString('latin1', 1, end)
45
+ if (type === 36) {
46
+ if (text === '-1') return buffer.length === end + 2 ? { value: null } : false
47
+ if (!/^(?:0|[1-9]\d{0,6})$/.test(text)) return false
48
+ const size = Number(text)
49
+ if (size > ENTRY_BYTES) return false
50
+ const full = end + 2 + size + 2
51
+ if (buffer.length < full) return undefined
52
+ if (buffer.length !== full || buffer[full - 2] !== 13 || buffer[full - 1] !== 10) return false
53
+ return { value: Buffer.from(buffer.subarray(end + 2, full - 2)) }
54
+ }
55
+ if (buffer.length !== end + 2) return false
56
+ if (type === 43 && text === 'OK') return { value: 'OK' }
57
+ if (type === 58 && /^(?:0|1)$/.test(text)) return { value: Number(text) }
58
+ return false // Errors include OOM/noeviction, NOAUTH, NOPERM and MOVED: no body/value is logged.
59
+ }
60
+ function publicHeaders(headers) {
61
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers) || Object.getPrototypeOf(headers) !== Object.prototype) return false
62
+ let bytes = 0
63
+ const entries = Object.entries(headers)
64
+ if (entries.length > 32) return false
65
+ for (const [key, value] of entries) {
66
+ if (!SAFE_RESPONSE.has(key.toLowerCase()) || typeof value !== 'string' || /[\r\n\x00]/.test(value) || value.length > 4096) return false
67
+ try { validateHeaderValue(key, value) } catch { return false }
68
+ if (key.toLowerCase() === 'cache-control' && privateControl(value)) return false
69
+ bytes += Buffer.byteLength(key) + Buffer.byteLength(value)
70
+ }
71
+ return bytes <= 16384
72
+ }
73
+ const lifetime = seconds => Number.isInteger(seconds) && seconds > 0 && seconds <= Number.MAX_SAFE_INTEGER / 2000 ? Math.min(seconds * 2000, MAX_TTL_MS) : 0
74
+ function record(value, seconds) {
75
+ if (!value || value.v !== 1 || value.seconds !== seconds || typeof value.out !== 'string' || Buffer.byteLength(value.out) > ENTRY_BYTES || !publicHeaders(value.h) || !Number.isSafeInteger(value.t)) return null
76
+ const age = Date.now() - value.t
77
+ if (age < 0 || age >= lifetime(seconds)) return null
78
+ return { out: value.out, h: value.h, t: value.t, bytes: Buffer.byteLength(value.out), cacheable: true, etag: 'W/"' + createHash('sha1').update(value.out).digest('base64url').slice(0, 22) + '"' }
79
+ }
80
+
81
+ /** Both environment variables and a real build identity are required. Invalid/partial opt-ins stay disabled. */
82
+ export function createRedisCache({ url, namespace, build, base = '/' } = {}) {
83
+ if (!url || !namespace) return null
84
+ const config = settings(url, namespace, build, base)
85
+ if (!config) return null
86
+ const owned = new Set()
87
+ // libc/libuv DNS cannot be canceled by destroying a socket. A timed-out lookup keeps its admission
88
+ // lease until its callback returns, so repeated misses cannot queue unlimited resolver work.
89
+ const leases = new Set()
90
+ let stopped = false
91
+ const key = identity => typeof identity === 'string' && identity.length <= 16384 ? config.prefix + digest(identity) : null
92
+ async function command(parts) {
93
+ if (stopped || leases.size >= MAX_ACTIVE) return null
94
+ const lease = { lookups: 0 }
95
+ leases.add(lease)
96
+ let socket, finish, receive, offset = 0, done = false
97
+ let buffer = Buffer.allocUnsafe(4096)
98
+ const completed = new Promise(resolve => { finish = value => {
99
+ if (done) return
100
+ done = true; clearTimeout(timer); const pending = receive; receive = null; pending?.(null)
101
+ if (socket) { owned.delete(socket); socket.destroy() }
102
+ if (!lease.lookups) leases.delete(lease)
103
+ resolve(value)
104
+ } })
105
+ const timer = setTimeout(() => finish(null), DEADLINE_MS)
106
+ const exchange = values => new Promise(resolve => {
107
+ if (done || stopped) return resolve(null)
108
+ offset = 0; receive = resolve
109
+ socket.write(frame(values), error => { if (error) finish(null) })
110
+ })
111
+ const lookup = (host, options, callback) => {
112
+ lease.lookups++
113
+ try {
114
+ dns.lookup(host, options, (...args) => {
115
+ lease.lookups--
116
+ if (done || stopped) {
117
+ if (!lease.lookups) leases.delete(lease)
118
+ callback(Object.assign(new Error('Cache lookup expired'), { code: 'ECANCELED' }))
119
+ } else callback(...args)
120
+ })
121
+ } catch (error) { lease.lookups--; throw error }
122
+ }
123
+ try {
124
+ socket = config.secure
125
+ ? tls.connect({ host: config.host, port: config.port, lookup, servername: net.isIP(config.host) ? undefined : config.host, rejectUnauthorized: true, checkServerIdentity: (_host, cert) => tls.checkServerIdentity(config.host, cert) })
126
+ : net.createConnection({ host: config.host, port: config.port, lookup })
127
+ owned.add(socket)
128
+ socket.on('error', () => finish(null)); socket.on('end', () => finish(null)); socket.on('close', () => finish(null))
129
+ socket.on('data', chunk => {
130
+ if (done) return
131
+ if (!receive || offset + chunk.length > ENTRY_BYTES + 1028) return finish(null)
132
+ if (offset + chunk.length > buffer.length) {
133
+ const grown = Buffer.allocUnsafe(Math.min(ENTRY_BYTES + 1028, Math.max(offset + chunk.length, buffer.length * 2)))
134
+ buffer.copy(grown, 0, 0, offset); buffer = grown
135
+ }
136
+ chunk.copy(buffer, offset); offset += chunk.length
137
+ const parsed = reply(buffer.subarray(0, offset))
138
+ if (parsed === false) return finish(null)
139
+ if (parsed) { const resolve = receive; receive = null; resolve(parsed.value) }
140
+ })
141
+ socket.once(config.secure ? 'secureConnect' : 'connect', async () => {
142
+ try {
143
+ if (config.password && await exchange(config.user ? ['AUTH', config.user, config.password] : ['AUTH', config.password]) !== 'OK') return finish(null)
144
+ if (config.db && await exchange(['SELECT', config.db]) !== 'OK') return finish(null)
145
+ finish(await exchange(parts))
146
+ } catch { finish(null) }
147
+ })
148
+ } catch { finish(null) }
149
+ return completed
150
+ }
151
+ return {
152
+ async get(identity, seconds) {
153
+ const target = key(identity)
154
+ if (!target || !lifetime(seconds)) return null
155
+ const body = await command(['GET', target])
156
+ if (!Buffer.isBuffer(body) || body.length > ENTRY_BYTES) return null
157
+ try { return record(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body)), seconds) } catch { return null }
158
+ },
159
+ async set(identity, value, seconds) {
160
+ const target = key(identity)
161
+ if (!target || !value?.cacheable || !lifetime(seconds)) return false
162
+ const envelope = { v: 1, out: value.out, h: value.h, t: value.t, seconds }
163
+ if (!record(envelope, seconds)) return false
164
+ let body
165
+ try { body = JSON.stringify(envelope) } catch { return false }
166
+ const remaining = lifetime(seconds) - (Date.now() - value.t)
167
+ if (Buffer.byteLength(body) > ENTRY_BYTES || remaining < 1) return false
168
+ return await command(['SET', target, body, 'PX', remaining]) === 'OK'
169
+ },
170
+ async delete(identity) { const target = key(identity); if (target) await command(['DEL', target]) },
171
+ close() { stopped = true; for (const socket of owned) socket.destroy(); owned.clear() },
172
+ }
173
+ }
@@ -0,0 +1,156 @@
1
+ // Server-only, single-project error transport. It accepts our schema, never arbitrary Sentry envelopes.
2
+ import { request as httpsRequest } from 'node:https'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { STATUS_CODES } from 'node:http'
5
+ import dns from 'node:dns'
6
+
7
+ const TYPES = new Set(['Error', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError', 'URIError', 'EvalError', 'AggregateError'])
8
+ const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
9
+ const MAX_BODY = 32768
10
+ const ERROR_CODES = { 400: 'SENTRY_INVALID_EVENT', 403: 'SENTRY_ORIGIN_REFUSED', 405: 'SENTRY_METHOD_NOT_ALLOWED', 408: 'SENTRY_BODY_TIMEOUT', 413: 'SENTRY_BODY_TOO_LARGE', 415: 'SENTRY_UNSUPPORTED_MEDIA', 429: 'SENTRY_RATE_LIMITED', 503: 'SENTRY_UNAVAILABLE' }
11
+ const setupError = () => Error('Sentry requires a valid private HTTPS BRAIN_SENTRY_DSN; self-hosted targets also require the exact private BRAIN_SENTRY_HOST. No DSN value was logged.')
12
+ export function parseDSN(raw, approvedHost) {
13
+ try {
14
+ if (typeof raw !== 'string' || raw.length > 2048 || /[\s\\]/.test(raw)) throw 0
15
+ const url = new URL(raw)
16
+ if (url.href !== raw || url.protocol !== 'https:' || url.password || url.search || url.hash || !/^[a-f0-9]{32}$/.test(url.username) || !/^\/[1-9][0-9]{0,19}$/.test(url.pathname)) throw 0
17
+ const hosted = /^(?:[a-z0-9-]+\.)*ingest(?:\.[a-z]{2})?\.sentry\.io$/.test(url.hostname) && !url.port
18
+ if (!hosted && (typeof approvedHost !== 'string' || url.host !== approvedHost)) throw 0
19
+ return Object.freeze({ endpoint: new URL('/api' + url.pathname + '/envelope/', url.origin), key: url.username })
20
+ } catch { throw setupError() }
21
+ }
22
+ const keys = (value, names) => value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === names.length && names.every(key => Object.hasOwn(value, key))
23
+ export function sanitizeEvent(value, assets) {
24
+ if (!keys(value, ['schemaVersion', 'type', 'frames']) || value.schemaVersion !== 1 || !TYPES.has(value.type) || !Array.isArray(value.frames) || value.frames.length < 1 || value.frames.length > 20) return null
25
+ const frames = []
26
+ for (const f of value.frames) {
27
+ if (!keys(f, ['asset', 'line', 'column', 'debugId']) || typeof f.asset !== 'string' || f.asset.length > 512 || !/^\/(?:[a-zA-Z0-9_.-]+\/)*[a-z0-9]{8}\.(?:js|jsbrain)$/.test(f.asset) || !UUID.test(f.debugId) || !Object.hasOwn(assets, f.asset) || assets[f.asset] !== f.debugId || !Number.isSafeInteger(f.line) || f.line < 1 || f.line > 9999999 || !Number.isSafeInteger(f.column) || f.column < 1 || f.column > 9999999) return null
28
+ frames.push({ filename: f.asset, abs_path: f.asset, lineno: f.line, colno: f.column, in_app: true })
29
+ }
30
+ const images = [...new Set(value.frames.map(f => f.asset))].map(asset => ({ type: 'sourcemap', code_file: asset, debug_id: assets[asset] }))
31
+ return { platform: 'javascript', level: 'error', exception: { values: [{ type: value.type, value: 'Application error', stacktrace: { frames: frames.reverse() } }] }, debug_meta: { images } }
32
+ }
33
+ function retrySeconds(headers, status, now) {
34
+ let seconds = 0
35
+ const limits = headers['x-sentry-rate-limits']
36
+ if (typeof limits === 'string') for (const part of limits.slice(0, 2048).split(',').slice(0, 32)) {
37
+ const [delay, categories] = part.trim().split(':')
38
+ if (categories !== undefined && (!categories || categories.split(';').includes('error'))) {
39
+ const n = Number(delay)
40
+ if (Number.isFinite(n) && n >= 0) seconds = Math.max(seconds, n)
41
+ }
42
+ }
43
+ if (status === 429) {
44
+ const raw = headers['retry-after']
45
+ const number = typeof raw === 'string' && raw.length <= 128 ? Number(raw) : NaN
46
+ const date = typeof raw === 'string' && raw.length <= 128 ? Date.parse(raw) : NaN
47
+ seconds = Math.max(seconds, Number.isFinite(number) ? number : Number.isFinite(date) ? (date - now) / 1000 : 60)
48
+ }
49
+ return Math.min(3600, Math.max(0, Math.ceil(seconds)))
50
+ }
51
+ export function createSentryTunnel({ dsn, approvedHost, assets, publicOrigin, request = httpsRequest, now = Date.now }) {
52
+ const target = parseDSN(dsn, approvedHost)
53
+ let ownOrigin
54
+ if (publicOrigin !== undefined) {
55
+ try {
56
+ const url = new URL(publicOrigin)
57
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) throw 0
58
+ ownOrigin = url.origin
59
+ } catch { throw Error('Sentry needs one canonical public HTTP(S) origin; set PUBLIC_SITE_URL for a TLS reverse proxy') }
60
+ }
61
+ if (!assets || typeof assets !== 'object' || Array.isArray(assets) || Object.keys(assets).length > 4096) throw Error('Sentry requires the build asset catalog; rebuild the app with sentry.enabled true')
62
+ const catalog = Object.freeze({ ...assets })
63
+ const clients = new Map(), pending = new Set(), leases = new Set()
64
+ let closed = false, inFlight = 0, cooldown = 0, minute = -1, total = 0
65
+ const answer = (res, status, wait) => {
66
+ if (res.destroyed || res.writableEnded) return
67
+ const body = status >= 400 ? JSON.stringify({ error: { code: ERROR_CODES[status] || 'SENTRY_REQUEST_FAILED', message: STATUS_CODES[status] || 'Request failed' } }) : ''
68
+ res.writeHead(status, { 'cache-control': 'no-store', ...(body ? { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(body) } : {}), ...(status === 405 ? { allow: 'POST' } : {}), ...(wait ? { 'retry-after': String(wait) } : {}) })
69
+ res.end(body)
70
+ }
71
+ const forward = (event, lease) => new Promise(resolve => {
72
+ if (closed) return resolve()
73
+ const eventId = randomUUID().replaceAll('-', '')
74
+ const payload = JSON.stringify({ event_id: eventId, timestamp: now() / 1000, ...event })
75
+ const envelope = JSON.stringify({ event_id: eventId, sent_at: new Date(now()).toISOString() }) + '\n' + JSON.stringify({ type: 'event', length: Buffer.byteLength(payload) }) + '\n' + payload + '\n'
76
+ let outgoing, response, settled = false
77
+ const finish = () => {
78
+ if (settled) return
79
+ settled = true; clearTimeout(timer); pending.delete(cancel)
80
+ outgoing?.destroy(); response?.destroy(); resolve()
81
+ }
82
+ const cancel = () => finish()
83
+ const timer = setTimeout(finish, 3000)
84
+ pending.add(cancel)
85
+ // Destroying a request cannot cancel libc/libuv DNS. Keep its admission lease until every native
86
+ // callback completes, even after timeout or close. An expired callback must never start a connection.
87
+ const lookup = (host, options, callback) => {
88
+ if (settled || closed) return callback(Object.assign(Error('Sentry lookup expired'), { code: 'ECANCELED' }))
89
+ lease.lookups++
90
+ try {
91
+ dns.lookup(host, options, (...args) => {
92
+ lease.lookups--
93
+ if (lease.finished && !lease.lookups) leases.delete(lease)
94
+ if (settled || closed) callback(Object.assign(Error('Sentry lookup expired'), { code: 'ECANCELED' }))
95
+ else callback(...args)
96
+ })
97
+ } catch (error) { lease.lookups--; throw error }
98
+ }
99
+ try {
100
+ outgoing = request(target.endpoint, {
101
+ method: 'POST', agent: false, rejectUnauthorized: true, maxHeaderSize: 8192, lookup,
102
+ headers: { 'content-type': 'application/x-sentry-envelope', 'content-length': Buffer.byteLength(envelope), 'x-sentry-auth': 'Sentry sentry_version=7,sentry_key=' + target.key + ',sentry_client=brain.native/1' },
103
+ }, incoming => {
104
+ response = incoming
105
+ const delay = retrySeconds(incoming.headers, incoming.statusCode, now())
106
+ if (delay) cooldown = Math.max(cooldown, now() + delay * 1000)
107
+ // No response body, redirects or retries. Destroy rather than buffering an untrusted body.
108
+ incoming.on('error', finish); finish()
109
+ })
110
+ outgoing.on('error', finish); outgoing.end(envelope)
111
+ } catch { finish() }
112
+ })
113
+ const handle = async (req, res, client) => {
114
+ if (closed) return answer(res, 503, 60)
115
+ if (req.method !== 'POST') return answer(res, 405)
116
+ let origin
117
+ try { origin = new URL(req.headers.origin) } catch { return answer(res, 403) }
118
+ const expected = ownOrigin || (req.socket.encrypted ? 'https://' : 'http://') + req.headers.host
119
+ if (origin.origin !== expected || origin.host !== req.headers.host || origin.origin !== req.headers.origin || req.headers['sec-fetch-site'] && req.headers['sec-fetch-site'] !== 'same-origin') return answer(res, 403)
120
+ if (req.headers['content-encoding'] || req.headers['content-length'] && (!/^\d{1,6}$/.test(req.headers['content-length']) || +req.headers['content-length'] > MAX_BODY)) return answer(res, 413)
121
+ const media = req.headers['content-type']
122
+ if (typeof media !== 'string' || !/^application\/json(?:\s*;\s*charset\s*=\s*(?:utf-8|"utf-8"))?$/i.test(media.trim())) return answer(res, 415)
123
+ const at = now(), bucket = Math.floor(at / 60000)
124
+ if (minute !== bucket) { minute = bucket; total = 0; clients.clear() }
125
+ if (cooldown > at) return answer(res, 429, Math.max(1, Math.ceil((cooldown - at) / 1000)))
126
+ const key = typeof client === 'string' && client.length <= 128 ? client : 'unknown'
127
+ if (inFlight >= 4 || leases.size >= 4 || total >= 120 || !clients.has(key) && clients.size >= 1024 || (clients.get(key) || 0) >= 8) return answer(res, 429, 60)
128
+ clients.set(key, (clients.get(key) || 0) + 1); total++; inFlight++
129
+ const lease = { lookups: 0, finished: false }
130
+ leases.add(lease)
131
+ let timer
132
+ try {
133
+ const raw = await new Promise(resolve => {
134
+ let size = 0, chunks = [], done = false
135
+ const finish = result => { if (done) return; done = true; clearTimeout(timer); pending.delete(cancel); req.removeListener('data', data); req.removeListener('end', end); req.removeListener('error', error); req.removeListener('aborted', error); chunks = []; resolve(result) }
136
+ const cancel = () => { finish(null); answer(res, 503, 60); req.destroy() }
137
+ const data = chunk => { size += chunk.length; if (size > MAX_BODY) { finish(null); req.resume() } else chunks.push(chunk) }
138
+ const end = () => finish(Buffer.concat(chunks).toString('utf8'))
139
+ const error = () => finish(null)
140
+ timer = setTimeout(() => { finish(null); answer(res, 408); req.destroy() }, 3000)
141
+ pending.add(cancel)
142
+ req.on('data', data); req.once('end', end); req.once('error', error); req.once('aborted', error)
143
+ })
144
+ if (closed) return answer(res, 503, 60)
145
+ if (raw === null) return answer(res, 413)
146
+ let value
147
+ try { value = JSON.parse(raw) } catch { return answer(res, 400) }
148
+ const event = sanitizeEvent(value, catalog)
149
+ value = null
150
+ if (!event) return answer(res, 400)
151
+ await forward(event, lease)
152
+ return answer(res, closed ? 503 : 202, closed ? 60 : undefined)
153
+ } finally { clearTimeout(timer); inFlight--; lease.finished = true; if (!lease.lookups) leases.delete(lease) }
154
+ }
155
+ return { handle, close() { closed = true; for (const cancel of pending) cancel(); pending.clear(); clients.clear() } }
156
+ }