loopback-fixture-server.mjs 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. /** Shared lifecycle for snapshot HTTP fixtures that bind an ephemeral loopback port. */
  2. import { createServer } from 'node:http'
  3. function listen(server) {
  4. return new Promise((resolve, reject) => {
  5. const onError = (error) => {
  6. server.off('error', onError)
  7. reject(error)
  8. }
  9. server.once('error', onError)
  10. try {
  11. server.listen(0, '127.0.0.1', () => {
  12. server.off('error', onError)
  13. resolve(undefined)
  14. })
  15. } catch (error) {
  16. server.off('error', onError)
  17. reject(error)
  18. }
  19. })
  20. }
  21. async function close(server) {
  22. if (!server.listening) return
  23. await new Promise((resolve, reject) => {
  24. server.close(error => error ? reject(error) : resolve(undefined))
  25. server.closeAllConnections()
  26. })
  27. }
  28. async function cleanup(server, onCleanup, label) {
  29. const errors = []
  30. try {
  31. onCleanup()
  32. } catch (error) {
  33. errors.push(error)
  34. }
  35. try {
  36. await close(server)
  37. } catch (error) {
  38. errors.push(error)
  39. }
  40. if (errors.length === 1) throw errors[0]
  41. if (errors.length > 1) throw new AggregateError(errors, `${label}: cleanup failed`)
  42. }
  43. /**
  44. * Start a loopback server as a Cordis effect and join cleanup with its setup.
  45. * @param ctx - Cordis context that owns the listener effect.
  46. * @param options - Fixture callbacks and the effect label used in diagnostics.
  47. */
  48. export async function applyLoopbackServerEffect(ctx, options) {
  49. const { label, onCleanup, onListening, requestListener } = options
  50. await ctx.effect(async () => {
  51. const server = createServer(requestListener)
  52. try {
  53. await listen(server)
  54. const address = server.address()
  55. if (address === null || typeof address === 'string') {
  56. throw new Error(`${label}: loopback listener has no TCP address`)
  57. }
  58. onListening(address)
  59. // Snapshot fixtures must never hold the process open past protocol shutdown.
  60. server.unref()
  61. return () => cleanup(server, onCleanup, label)
  62. } catch (cause) {
  63. try {
  64. await cleanup(server, onCleanup, label)
  65. } catch (cleanupError) {
  66. throw new AggregateError([cause, cleanupError], `${label}: setup and cleanup failed`)
  67. }
  68. throw cause
  69. }
  70. }, label)
  71. }