queue.spec.ts 28 KB

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