1
0

queue.spec.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. /** Controlled source and engine completions exercise admission and shared content ownership. */
  2. import { DocumentSourceKey, DocumentRendererGeneration, type DocumentRenderRequest } from '@deepseek-ai/dsh-document-render'
  3. import { expect, it, onTestFinished, vi } from 'vitest'
  4. import { RenderQueue } from '../src/queue.ts'
  5. import { Config } from '../src/index.ts'
  6. const output = { pdf: new Uint8Array([37, 80, 68, 70]), missingFonts: ['Font'] }
  7. function source(key: string, byte = 1, priority: DocumentRenderRequest['priority'] = 'foreground') {
  8. const read = vi.fn<DocumentRenderRequest['source']['read']>().mockResolvedValue({ bytes: new Uint8Array([byte]), version: 'v1' })
  9. const request: DocumentRenderRequest = { extension: 'docx', priority, source: { key: DocumentSourceKey(key), version: 'v1', bytes: 1, read } }
  10. return { read, request }
  11. }
  12. function harness(config: Partial<Config> = {}) {
  13. const convert = vi.fn<ConstructorParameters<typeof RenderQueue>[2]>().mockResolvedValue(output)
  14. const queue = new RenderQueue(Config(config), DocumentRendererGeneration('test'), convert)
  15. onTestFinished(() => queue.dispose())
  16. return { queue, convert }
  17. }
  18. it('shares authorized source metadata before reading and content across distinct source paths', async () => {
  19. const h = harness()
  20. const entered = Promise.withResolvers<undefined>()
  21. const complete = Promise.withResolvers<typeof output>()
  22. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  23. const a = source('a'), b = source('b')
  24. const first = h.queue.read(a.request)
  25. await entered.promise
  26. const same = h.queue.read(a.request)
  27. const equalContent = h.queue.read(b.request)
  28. try {
  29. await vi.waitFor(() => { expect(b.read).toHaveBeenCalledOnce() })
  30. expect(a.read).toHaveBeenCalledOnce()
  31. expect(h.convert).toHaveBeenCalledOnce()
  32. } finally { complete.resolve(output) }
  33. const results = await Promise.all([first, same, equalContent])
  34. expect(new Set(results.map(result => result.cacheKey)).size).toBe(1)
  35. results[0].pdf[0] = 0
  36. results[0].missingFonts.length = 0
  37. expect(results[1].pdf).toEqual(output.pdf)
  38. expect((await h.queue.read(b.request)).missingFonts).toEqual(['Font'])
  39. expect(b.read).toHaveBeenCalledOnce()
  40. })
  41. it('continues queued work when the first queued reader cancels immediately after admission', async () => {
  42. const h = harness({ maxConcurrentConversions: 1 })
  43. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  44. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  45. const first = h.queue.read(source('active', 1).request)
  46. await entered.promise
  47. const cancelled = new AbortController()
  48. const second = source('cancelled', 2), third = source('later', 3)
  49. second.read.mockImplementation(async () => {
  50. cancelled.abort()
  51. return { bytes: new Uint8Array([2]), version: 'v1' }
  52. })
  53. const queued = h.queue.read(second.request, cancelled.signal)
  54. const rejected = expect(queued).rejects.toMatchObject({ name: 'AbortError' })
  55. const later = h.queue.read(third.request)
  56. try {
  57. expect(second.read).not.toHaveBeenCalled()
  58. expect(third.read).not.toHaveBeenCalled()
  59. } finally { complete.resolve(output) }
  60. await first
  61. await rejected
  62. await expect(later).resolves.toMatchObject(output)
  63. expect(second.read).toHaveBeenCalledOnce()
  64. expect(third.read).toHaveBeenCalledOnce()
  65. expect(h.convert).toHaveBeenCalledTimes(2)
  66. })
  67. it('promotes a queued prewarm when a foreground reader joins, without another source read', async () => {
  68. const h = harness({ maxConcurrentConversions: 1 })
  69. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  70. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  71. const first = h.queue.read(source('active', 1).request)
  72. await entered.promise
  73. const order: string[] = []
  74. const a = source('a', 2, 'background'), b = source('b', 3, 'background')
  75. a.read.mockImplementation(async () => { order.push('a'); return { bytes: new Uint8Array([2]), version: 'v1' } })
  76. b.read.mockImplementation(async () => { order.push('b'); return { bytes: new Uint8Array([3]), version: 'v1' } })
  77. const backgroundA = h.queue.read(a.request), backgroundB = h.queue.read(b.request)
  78. const foreground = h.queue.read({ ...b.request, priority: 'foreground' })
  79. expect(a.read).not.toHaveBeenCalled()
  80. expect(b.read).not.toHaveBeenCalled()
  81. complete.resolve(output)
  82. await Promise.all([first, backgroundA, backgroundB, foreground])
  83. expect(order).toEqual(['b', 'a'])
  84. expect(b.read).toHaveBeenCalledOnce()
  85. })
  86. it('evicts queued speculation for foreground work and bounds the metadata queue', async () => {
  87. const h = harness({ maxConcurrentConversions: 1, maxQueuedJobs: 1 })
  88. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  89. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  90. const first = h.queue.read(source('active').request)
  91. await entered.promise
  92. const prewarm = source('prewarm', 2, 'background')
  93. const discarded = expect(h.queue.read(prewarm.request)).rejects.toMatchObject({ code: 'busy' })
  94. const overflow = source('background-overflow', 4, 'background')
  95. await expect(h.queue.read(overflow.request)).rejects.toMatchObject({ code: 'busy' })
  96. expect(overflow.read).not.toHaveBeenCalled()
  97. const requested = source('requested', 3)
  98. const next = h.queue.read(requested.request)
  99. await expect(h.queue.read(source('overflow', 4).request)).rejects.toMatchObject({ code: 'busy' })
  100. await discarded
  101. expect(prewarm.read).not.toHaveBeenCalled()
  102. expect(requested.read).not.toHaveBeenCalled()
  103. complete.resolve(output)
  104. await Promise.all([first, next])
  105. expect(requested.read).toHaveBeenCalledOnce()
  106. })
  107. it('refuses disabled prewarming before reading a source while admitting foreground work', async () => {
  108. const h = harness({ maxBackgroundConversions: 0 })
  109. const a = source('a', 1, 'background')
  110. await expect(h.queue.read(a.request)).rejects.toMatchObject({ code: 'busy' })
  111. expect(a.read).not.toHaveBeenCalled()
  112. expect(h.convert).not.toHaveBeenCalled()
  113. await h.queue.read({ ...a.request, priority: 'foreground' })
  114. expect(a.read).toHaveBeenCalledOnce()
  115. })
  116. it('refuses disabled prewarming joins and releases cancelled foreground capacity', async () => {
  117. const h = harness({ maxBackgroundConversions: 0, maxConcurrentConversions: 1, maxQueuedJobs: 1, maxReaders: 5 })
  118. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  119. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  120. const running = source('running', 1), queued = source('queued', 2), later = source('later', 3)
  121. const foreground = new AbortController(), background = new AbortController()
  122. const pending: Promise<unknown>[] = [h.queue.read(running.request)]
  123. const rejected = vi.fn()
  124. try {
  125. await entered.promise
  126. pending.push(h.queue.read(queued.request, foreground.signal).catch((error: unknown) => error))
  127. for (const item of [running, queued]) {
  128. pending.push(h.queue.read({ ...item.request, priority: 'background' }, background.signal).catch(rejected))
  129. }
  130. await expect.poll(() => rejected.mock.calls.length).toBe(2)
  131. expect(rejected.mock.calls).toEqual([[expect.objectContaining({ code: 'busy' })], [expect.objectContaining({ code: 'busy' })]])
  132. foreground.abort()
  133. const next = h.queue.read(later.request)
  134. pending.push(next)
  135. expect(queued.read).not.toHaveBeenCalled()
  136. expect(later.read).not.toHaveBeenCalled()
  137. complete.resolve(output)
  138. await expect(next).resolves.toMatchObject(output)
  139. await expect(h.queue.read({ ...running.request, priority: 'background' })).resolves.toMatchObject(output)
  140. expect(running.read).toHaveBeenCalledOnce()
  141. expect(later.read).toHaveBeenCalledOnce()
  142. expect(h.convert).toHaveBeenCalledTimes(2)
  143. } finally {
  144. foreground.abort()
  145. background.abort()
  146. complete.resolve(output)
  147. await Promise.allSettled(pending)
  148. }
  149. })
  150. it('reserves foreground capacity while limiting concurrent prewarming', async () => {
  151. const h = harness({ maxConcurrentConversions: 2, maxBackgroundConversions: 1 })
  152. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  153. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  154. const first = h.queue.read(source('first', 1, 'background').request)
  155. await entered.promise
  156. const second = source('second', 2, 'background')
  157. const background = h.queue.read(second.request)
  158. const foreground = source('foreground', 3)
  159. await h.queue.read(foreground.request)
  160. expect(second.read).not.toHaveBeenCalled()
  161. complete.resolve(output)
  162. await Promise.all([first, background])
  163. })
  164. it('holds reserved source capacity until canceled engine work actually settles', async () => {
  165. const h = harness({ maxConcurrentConversions: 2, maxInputBytes: 1, maxSourceBytes: 1 })
  166. const entered = Promise.withResolvers<AbortSignal>(), complete = Promise.withResolvers<typeof output>()
  167. h.convert.mockImplementationOnce((_bytes, _extension, signal: AbortSignal) => { entered.resolve(signal); return complete.promise })
  168. const caller = new AbortController()
  169. const firstSource = source('first'), nextSource = source('next', 2)
  170. const first = expect(h.queue.read(firstSource.request, caller.signal)).rejects.toMatchObject({ name: 'AbortError' })
  171. const signal = await entered.promise
  172. caller.abort()
  173. await first
  174. expect(signal.aborted).toBe(true)
  175. const next = h.queue.read(nextSource.request)
  176. expect(nextSource.read).not.toHaveBeenCalled()
  177. complete.resolve(output)
  178. await next
  179. await h.queue.read(firstSource.request)
  180. expect(firstSource.read).toHaveBeenCalledTimes(2)
  181. })
  182. it('bounds shared readers and leaves other readers alive after cancellation', async () => {
  183. const h = harness({ maxReaders: 2 })
  184. const entered = Promise.withResolvers<AbortSignal>(), complete = Promise.withResolvers<typeof output>()
  185. h.convert.mockImplementationOnce((_bytes, _extension, signal: AbortSignal) => { entered.resolve(signal); return complete.promise })
  186. const a = source('a'), caller = new AbortController()
  187. const first = expect(h.queue.read(a.request, caller.signal)).rejects.toMatchObject({ name: 'AbortError' })
  188. const signal = await entered.promise
  189. const second = h.queue.read(a.request)
  190. await expect(h.queue.read(a.request)).rejects.toMatchObject({ code: 'busy' })
  191. caller.abort()
  192. await first
  193. expect(signal.aborted).toBe(false)
  194. complete.resolve(output)
  195. await second
  196. })
  197. it('keeps digest-shared readers alive when the original source reader leaves', async () => {
  198. const h = harness()
  199. const entered = Promise.withResolvers<AbortSignal>(), complete = Promise.withResolvers<typeof output>()
  200. h.convert.mockImplementationOnce((_bytes, _extension, signal: AbortSignal) => { entered.resolve(signal); return complete.promise })
  201. const a = source('a'), b = source('b'), caller = new AbortController()
  202. const first = expect(h.queue.read(a.request, caller.signal)).rejects.toMatchObject({ name: 'AbortError' })
  203. const signal = await entered.promise
  204. const second = h.queue.read(b.request)
  205. await vi.waitFor(() => { expect(b.read).toHaveBeenCalledOnce() })
  206. caller.abort()
  207. await first
  208. expect(signal.aborted).toBe(false)
  209. complete.resolve(output)
  210. await second
  211. expect(h.convert).toHaveBeenCalledOnce()
  212. })
  213. it('rejects source version changes and oversized reads without converting or retaining them', async () => {
  214. const h = harness({ maxInputBytes: 2, maxSourceBytes: 2 })
  215. const a = source('a')
  216. a.read.mockResolvedValueOnce({ bytes: new Uint8Array([1]), version: 'v2' })
  217. await expect(h.queue.read(a.request)).rejects.toMatchObject({ code: 'source-changed' })
  218. a.read.mockResolvedValueOnce({ bytes: new Uint8Array([1, 2]), version: 'v1' })
  219. await expect(h.queue.read(a.request)).rejects.toMatchObject({ code: 'input-too-large' })
  220. expect(h.convert).not.toHaveBeenCalled()
  221. await h.queue.read(a.request)
  222. expect(a.read).toHaveBeenCalledTimes(3)
  223. expect(a.read.mock.calls[0]![1]).toBe(1)
  224. })
  225. it('reserves the input cap for an unknown stat size and rejects known overflow before reading', async () => {
  226. const h = harness({ maxInputBytes: 2, maxSourceBytes: 2 })
  227. const a = source('a')
  228. await expect(h.queue.read({ ...a.request, source: { ...a.request.source, bytes: 3 } })).rejects.toMatchObject({ code: 'input-too-large' })
  229. expect(a.read).not.toHaveBeenCalled()
  230. const { bytes: _bytes, ...unknown } = a.request.source
  231. await h.queue.read({ ...a.request, source: unknown })
  232. expect(a.read.mock.calls[0]![1]).toBe(2)
  233. })
  234. it('evicts least-recently-used content and bounds pre-read aliases independently', async () => {
  235. const h = harness({ maxCachedEntries: 2, maxCachedBytes: 8, maxSourceEntries: 1 })
  236. const a = source('a', 1), alias = source('alias', 1), b = source('b', 2), c = source('c', 3)
  237. await h.queue.read(a.request)
  238. await h.queue.read(alias.request)
  239. await h.queue.read(a.request)
  240. expect(a.read).toHaveBeenCalledTimes(2)
  241. expect(h.convert).toHaveBeenCalledOnce()
  242. await h.queue.read(b.request)
  243. await h.queue.read(a.request)
  244. await h.queue.read(c.request)
  245. await h.queue.read(b.request)
  246. expect(h.convert).toHaveBeenCalledTimes(4)
  247. })
  248. it('does not retain failures or PDFs above the retention budget', async () => {
  249. const h = harness({ maxCachedBytes: 1 })
  250. const a = source('a')
  251. h.convert.mockRejectedValueOnce(new Error('engine failure'))
  252. await expect(h.queue.read(a.request)).rejects.toThrow('engine failure')
  253. await h.queue.read(a.request)
  254. await h.queue.read(a.request)
  255. expect(h.convert).toHaveBeenCalledTimes(3)
  256. })
  257. it('evicts every alias of the least-recently-used PDF while preserving other aliases', async () => {
  258. const h = harness({ maxCachedEntries: 2 })
  259. const a = source('a', 1), alias = source('alias', 1), b = source('b', 2), c = source('c', 3)
  260. await h.queue.read(a.request)
  261. await h.queue.read(alias.request)
  262. await h.queue.read(b.request)
  263. await h.queue.read(c.request)
  264. await h.queue.read(b.request)
  265. expect(b.read).toHaveBeenCalledOnce()
  266. await h.queue.read(a.request)
  267. await h.queue.read(alias.request)
  268. expect(a.read).toHaveBeenCalledTimes(2)
  269. expect(alias.read).toHaveBeenCalledTimes(2)
  270. expect(h.convert).toHaveBeenCalledTimes(4)
  271. })
  272. it('keeps a synchronous abort replacement shareable after the old conversion settles late', async () => {
  273. const h = harness({ maxConcurrentConversions: 2 })
  274. const firstEntered = Promise.withResolvers<undefined>(), firstComplete = Promise.withResolvers<typeof output>()
  275. const replacementEntered = Promise.withResolvers<undefined>(), replacementComplete = Promise.withResolvers<typeof output>()
  276. const replacementSpawned = Promise.withResolvers<{ work: ReturnType<RenderQueue['read']> }>()
  277. const a = source('a'), caller = new AbortController()
  278. const pending: Promise<unknown>[] = []
  279. h.convert.mockImplementationOnce((_bytes, _extension, signal) => {
  280. signal.addEventListener('abort', () => {
  281. const work = h.queue.read(a.request)
  282. pending.push(work)
  283. replacementSpawned.resolve({ work })
  284. }, { once: true })
  285. firstEntered.resolve(undefined)
  286. return firstComplete.promise
  287. })
  288. .mockImplementationOnce(() => { replacementEntered.resolve(undefined); return replacementComplete.promise })
  289. const first = expect(h.queue.read(a.request, caller.signal)).rejects.toMatchObject({ cause: 'reader left' })
  290. pending.push(first)
  291. try {
  292. await firstEntered.promise
  293. caller.abort('reader left')
  294. await first
  295. const { work: replacement } = await replacementSpawned.promise
  296. await replacementEntered.promise
  297. const next = h.queue.read(source('next', 2).request)
  298. pending.push(next)
  299. firstComplete.resolve(output)
  300. await next
  301. const sameSource = h.queue.read(a.request)
  302. const alias = source('alias', 1, 'background')
  303. const sameContent = h.queue.read(alias.request)
  304. pending.push(sameSource, sameContent)
  305. await vi.waitFor(() => { expect(alias.read).toHaveBeenCalledOnce() })
  306. expect(a.read).toHaveBeenCalledTimes(2)
  307. expect(h.convert).toHaveBeenCalledTimes(3)
  308. replacementComplete.resolve(output)
  309. const results = await Promise.all([replacement, sameSource, sameContent])
  310. expect(new Set(results.map(result => result.cacheKey)).size).toBe(1)
  311. } finally {
  312. firstComplete.resolve(output)
  313. replacementComplete.resolve(output)
  314. await Promise.allSettled(pending)
  315. }
  316. })
  317. it('separates Office extensions and renderer generations in content identity', async () => {
  318. const h = harness(), a = source('a')
  319. const first = await h.queue.read(a.request)
  320. const otherFormat = await h.queue.read({ ...a.request, extension: 'pptx' })
  321. const other = new RenderQueue(Config({}), DocumentRendererGeneration('replacement'), h.convert)
  322. onTestFinished(() => other.dispose())
  323. const replacement = await other.read(a.request)
  324. expect(first.cacheKey).not.toBe(otherFormat.cacheKey)
  325. expect(first.cacheKey).not.toBe(replacement.cacheKey)
  326. })
  327. it('reserves the final reader admission for foreground interest', async () => {
  328. const h = harness({ maxReaders: 2 })
  329. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  330. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  331. const a = source('a', 1, 'background')
  332. const background = h.queue.read(a.request)
  333. await entered.promise
  334. await expect(h.queue.read(a.request)).rejects.toMatchObject({ code: 'busy' })
  335. const foreground = h.queue.read({ ...a.request, priority: 'foreground' })
  336. complete.resolve(output)
  337. await Promise.all([background, foreground])
  338. expect(a.read).toHaveBeenCalledOnce()
  339. })
  340. it('serves ready PDFs while every outstanding reader slot is occupied', async () => {
  341. const h = harness({ maxReaders: 1 }), cached = source('cached')
  342. await h.queue.read(cached.request)
  343. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  344. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  345. const pending = h.queue.read(source('active', 2).request)
  346. try {
  347. await entered.promise
  348. await expect(h.queue.read(cached.request)).resolves.toMatchObject(output)
  349. expect(cached.read).toHaveBeenCalledOnce()
  350. expect(h.convert).toHaveBeenCalledTimes(2)
  351. } finally { complete.resolve(output); await pending }
  352. })
  353. it('keeps background reservations behind foreground work waiting for source capacity', async () => {
  354. const h = harness({ maxInputBytes: 50, maxSourceBytes: 60, maxConcurrentConversions: 2 })
  355. const running = source('running', 1), foreground = source('foreground', 2), background = source('background', 3, 'background')
  356. const entered = Promise.withResolvers<undefined>(), foregroundEntered = Promise.withResolvers<undefined>()
  357. const complete = Promise.withResolvers<typeof output>(), foregroundComplete = Promise.withResolvers<typeof output>()
  358. const order: number[] = []
  359. h.convert.mockImplementation((bytes) => {
  360. order.push(bytes[0]!)
  361. if (bytes[0] === 1) { entered.resolve(undefined); return complete.promise }
  362. if (bytes[0] === 2) { foregroundEntered.resolve(undefined); return foregroundComplete.promise }
  363. return Promise.resolve(output)
  364. })
  365. const first = h.queue.read({ ...running.request, source: { ...running.request.source, bytes: 15 } })
  366. await entered.promise
  367. const next = h.queue.read({ ...foreground.request, source: { ...foreground.request.source, bytes: 48 } })
  368. const speculative = h.queue.read({ ...background.request, source: { ...background.request.source, bytes: 45 } })
  369. try {
  370. expect(foreground.read).not.toHaveBeenCalled()
  371. expect(background.read).not.toHaveBeenCalled()
  372. complete.resolve(output)
  373. await foregroundEntered.promise
  374. expect(order).toEqual([1, 2])
  375. } finally {
  376. complete.resolve(output)
  377. foregroundComplete.resolve(output)
  378. await Promise.allSettled([first, next, speculative])
  379. }
  380. expect(order).toEqual([1, 2, 3])
  381. })
  382. it('starts eligible background work when its queued foreground blocker is cancelled', async () => {
  383. const h = harness({ maxInputBytes: 50, maxSourceBytes: 60, maxConcurrentConversions: 2 })
  384. const running = source('running', 1), foreground = source('foreground', 2), background = source('background', 3, 'background')
  385. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  386. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  387. const first = h.queue.read({ ...running.request, source: { ...running.request.source, bytes: 15 } })
  388. const caller = new AbortController()
  389. let speculative: Promise<unknown> | undefined
  390. try {
  391. await entered.promise
  392. const cancelled = expect(h.queue.read({ ...foreground.request, source: { ...foreground.request.source, bytes: 48 } }, caller.signal))
  393. .rejects.toMatchObject({ name: 'AbortError' })
  394. speculative = h.queue.read({ ...background.request, source: { ...background.request.source, bytes: 45 } })
  395. expect(background.read).not.toHaveBeenCalled()
  396. caller.abort()
  397. await cancelled
  398. expect(foreground.read).not.toHaveBeenCalled()
  399. expect(background.read).toHaveBeenCalledOnce()
  400. await speculative
  401. expect(h.convert).toHaveBeenCalledTimes(2)
  402. } finally { caller.abort(); complete.resolve(output); await Promise.allSettled([first, speculative]) }
  403. })
  404. it('starts eligible background work after a queued promotion loses its final foreground reader', async () => {
  405. const h = harness({ maxInputBytes: 50, maxSourceBytes: 60, maxConcurrentConversions: 2 })
  406. const running = source('running', 1), promoted = source('promoted', 2, 'background'), background = source('background', 3, 'background')
  407. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  408. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  409. const first = h.queue.read({ ...running.request, source: { ...running.request.source, bytes: 15 } })
  410. const callers = [new AbortController(), new AbortController()]
  411. const pending: Promise<unknown>[] = [first]
  412. try {
  413. await entered.promise
  414. const request = { ...promoted.request, source: { ...promoted.request.source, bytes: 48 } }
  415. pending.push(h.queue.read(request))
  416. const cancelled = callers.map(caller => expect(h.queue.read({ ...request, priority: 'foreground' }, caller.signal))
  417. .rejects.toMatchObject({ name: 'AbortError' }))
  418. pending.push(...cancelled)
  419. pending.push(h.queue.read({ ...background.request, source: { ...background.request.source, bytes: 45 } }))
  420. expect(background.read).not.toHaveBeenCalled()
  421. callers[0]!.abort()
  422. await cancelled[0]
  423. expect(background.read).not.toHaveBeenCalled()
  424. callers[1]!.abort()
  425. await cancelled[1]
  426. expect(promoted.read).not.toHaveBeenCalled()
  427. expect(background.read).toHaveBeenCalledOnce()
  428. } finally {
  429. for (const caller of callers) caller.abort()
  430. complete.resolve(output)
  431. await Promise.allSettled(pending)
  432. }
  433. })
  434. it('counts a demoted queued prewarm against background concurrency when it starts', async () => {
  435. const h = harness({ maxConcurrentConversions: 2, maxBackgroundConversions: 1 })
  436. const complete = Promise.withResolvers<typeof output>(), prewarmComplete = Promise.withResolvers<typeof output>()
  437. const prewarmEntered = Promise.withResolvers<undefined>()
  438. h.convert.mockImplementation((bytes) => {
  439. if (bytes[0] === 1 || bytes[0] === 2) return complete.promise
  440. if (bytes[0] === 3) { prewarmEntered.resolve(undefined); return prewarmComplete.promise }
  441. return Promise.resolve(output)
  442. })
  443. const first = h.queue.read(source('first', 1).request), second = h.queue.read(source('second', 2).request)
  444. const prewarm = source('promoted', 3, 'background'), other = source('other', 4, 'background')
  445. const warming = h.queue.read(prewarm.request), waiting = h.queue.read(other.request)
  446. const caller = new AbortController()
  447. const cancelled = expect(h.queue.read({ ...prewarm.request, priority: 'foreground' }, caller.signal))
  448. .rejects.toMatchObject({ name: 'AbortError' })
  449. try {
  450. expect(prewarm.read).not.toHaveBeenCalled()
  451. caller.abort()
  452. await cancelled
  453. complete.resolve(output)
  454. await prewarmEntered.promise
  455. await h.queue.read(source('foreground', 5).request)
  456. expect(other.read).not.toHaveBeenCalled()
  457. } finally {
  458. caller.abort()
  459. complete.resolve(output)
  460. prewarmComplete.resolve(output)
  461. await Promise.allSettled([first, second, warming, waiting, cancelled])
  462. }
  463. })
  464. it('releases source indexes when the last reader for each digest-shared path leaves', async () => {
  465. const h = harness({ maxReaders: 2, maxSourceEntries: 2 })
  466. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  467. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  468. const first = h.queue.read(source('retained').request)
  469. await entered.promise
  470. // Both retained indexes must stay bounded while the underlying converter is blocked.
  471. const indexes = h.queue as unknown as { sources: Map<string, { sources: Set<string> }> }
  472. const retained = [...indexes.sources.values()][0]!
  473. try {
  474. for (let index = 0; index < 5; index++) {
  475. const alias = source(`cancelled-${index}`), caller = new AbortController()
  476. const joined = h.queue.read(alias.request, caller.signal)
  477. const rejected = expect(joined).rejects.toMatchObject({ name: 'AbortError' })
  478. await vi.waitFor(() => { expect(retained.sources.size).toBe(2) })
  479. caller.abort()
  480. await rejected
  481. expect(indexes.sources.size).toBe(1)
  482. expect(retained.sources.size).toBe(1)
  483. }
  484. expect(h.convert).toHaveBeenCalledOnce()
  485. } finally { complete.resolve(output); await first }
  486. })
  487. it('rereads a cancelled source on reopen while sharing its running or completed conversion', async () => {
  488. const h = harness({ maxReaders: 2, maxSourceEntries: 2 })
  489. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  490. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  491. const first = h.queue.read(source('retained').request), reopened = source('reopened')
  492. try {
  493. await entered.promise
  494. for (const count of [1, 2]) {
  495. const caller = new AbortController()
  496. const cancelled = expect(h.queue.read(reopened.request, caller.signal)).rejects.toMatchObject({ name: 'AbortError' })
  497. try { await vi.waitFor(() => { expect(reopened.read).toHaveBeenCalledTimes(count) }) }
  498. finally { caller.abort(); await cancelled }
  499. }
  500. complete.resolve(output)
  501. await first
  502. await h.queue.read(reopened.request)
  503. expect(reopened.read).toHaveBeenCalledTimes(3)
  504. await h.queue.read(reopened.request)
  505. expect(reopened.read).toHaveBeenCalledTimes(3)
  506. expect(h.convert).toHaveBeenCalledOnce()
  507. } finally { complete.resolve(output); await first }
  508. })
  509. it('keeps speculative admission occupied after promotion while allowing other foreground work', async () => {
  510. const h = harness({ maxConcurrentConversions: 2, maxBackgroundConversions: 1 })
  511. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  512. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  513. const original = source('original', 1, 'background'), waiting = source('waiting', 2, 'background')
  514. const first = h.queue.read(original.request)
  515. await entered.promise
  516. const promoted = h.queue.read({ ...original.request, priority: 'foreground' })
  517. const speculative = h.queue.read(waiting.request)
  518. try {
  519. await h.queue.read(source('requested', 3).request)
  520. expect(waiting.read).not.toHaveBeenCalled()
  521. } finally { complete.resolve(output); await Promise.all([first, promoted, speculative]) }
  522. expect(waiting.read).toHaveBeenCalledOnce()
  523. })
  524. it('classifies provider disposal independently from caller cancellation', async () => {
  525. const h = harness()
  526. const entered = Promise.withResolvers<undefined>(), complete = Promise.withResolvers<typeof output>()
  527. h.convert.mockImplementationOnce(() => { entered.resolve(undefined); return complete.promise })
  528. const pending = expect(h.queue.read(source('active').request)).rejects.toMatchObject({ code: 'unavailable' })
  529. await entered.promise
  530. const closing = h.queue.dispose()
  531. try {
  532. await pending
  533. await expect(h.queue.read(source('later').request)).rejects.toMatchObject({ code: 'unavailable' })
  534. } finally { complete.resolve(output); await closing }
  535. })