lepus 0.2.2 → 0.3.0

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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile.lock +3 -3
  4. data/README.md +104 -9
  5. data/bin/web-demo +592 -0
  6. data/docs/brand/BRAND.md +67 -0
  7. data/docs/brand/apple-touch-icon.png +0 -0
  8. data/docs/brand/favicon-16.png +0 -0
  9. data/docs/brand/favicon-32.png +0 -0
  10. data/docs/brand/favicon.ico +0 -0
  11. data/docs/brand/favicon.svg +1 -0
  12. data/docs/brand/icon-512.png +0 -0
  13. data/docs/brand/lockup-lepus-mono.svg +1 -0
  14. data/docs/brand/lockup-lepus-on-dark.svg +1 -0
  15. data/docs/brand/lockup-lepus-on-light.svg +1 -0
  16. data/docs/brand/lockup-lepus-saga-mono.svg +1 -0
  17. data/docs/brand/lockup-lepus-saga-on-dark.svg +1 -0
  18. data/docs/brand/lockup-lepus-saga-on-light.svg +1 -0
  19. data/docs/brand/lockup-lepus-saga.svg +1 -0
  20. data/docs/brand/lockup-lepus.svg +1 -0
  21. data/docs/brand/mark-mono.svg +1 -0
  22. data/docs/brand/mark-on-dark.svg +1 -0
  23. data/docs/brand/mark-on-light.svg +1 -0
  24. data/docs/brand/mark-small-mono.svg +1 -0
  25. data/docs/brand/mark-small-on-dark.svg +1 -0
  26. data/docs/brand/mark-small-on-light.svg +1 -0
  27. data/docs/brand/mark-small.svg +1 -0
  28. data/docs/brand/mark.svg +1 -0
  29. data/docs/configuration.md +1 -0
  30. data/docs/consumers.md +39 -5
  31. data/docs/getting-started.md +1 -1
  32. data/docs/images/lepus-web.png +0 -0
  33. data/docs/middleware.md +1 -1
  34. data/docs/producers.md +41 -0
  35. data/docs/testing.md +67 -48
  36. data/docs/web.md +39 -13
  37. data/gemfiles/Gemfile.rails-5.2.lock +2 -2
  38. data/gemfiles/Gemfile.rails-6.1.lock +4 -4
  39. data/gemfiles/Gemfile.rails-7.2.lock +1 -1
  40. data/gemfiles/Gemfile.rails-8.0.lock +1 -1
  41. data/lib/lepus/configuration.rb +6 -0
  42. data/lib/lepus/consumer.rb +34 -0
  43. data/lib/lepus/consumers/config.rb +8 -7
  44. data/lib/lepus/consumers/stats.rb +15 -2
  45. data/lib/lepus/delay.rb +180 -0
  46. data/lib/lepus/primitive/string.rb +2 -12
  47. data/lib/lepus/producer.rb +6 -1
  48. data/lib/lepus/producers/definition.rb +3 -1
  49. data/lib/lepus/publisher.rb +29 -3
  50. data/lib/lepus/testing/rspec_matchers.rb +20 -1
  51. data/lib/lepus/testing.rb +2 -0
  52. data/lib/lepus/version.rb +1 -1
  53. data/lib/lepus/web/api.rb +45 -0
  54. data/lib/lepus/web/app.rb +19 -16
  55. data/lib/lepus/web/management_api.rb +125 -2
  56. data/lib/lepus/web/topology.rb +341 -0
  57. data/lib/lepus/web.rb +17 -18
  58. data/lib/lepus.rb +1 -0
  59. data/web/assets/app.css +862 -0
  60. data/web/assets/apple-touch-icon.png +0 -0
  61. data/web/assets/favicon-32.png +0 -0
  62. data/web/assets/favicon.svg +1 -0
  63. data/web/assets/fonts/overpass/OFL.txt +93 -0
  64. data/web/assets/fonts/overpass/overpass-latin-wght.woff2 +0 -0
  65. data/web/assets/js/api.js +27 -0
  66. data/web/assets/js/dom.js +95 -0
  67. data/web/assets/js/format.js +63 -0
  68. data/web/assets/js/live.js +74 -0
  69. data/web/assets/js/main.js +156 -0
  70. data/web/assets/js/map.js +606 -0
  71. data/web/assets/js/model.js +262 -0
  72. data/web/assets/js/rates.js +161 -0
  73. data/web/assets/js/theme.js +8 -0
  74. data/web/assets/js/views/connections.js +61 -0
  75. data/web/assets/js/views/overview.js +229 -0
  76. data/web/assets/js/views/processes.js +101 -0
  77. data/web/assets/js/views/queues.js +456 -0
  78. data/web/index.html +58 -153
  79. data/web/sw.js +14 -143
  80. metadata +47 -12
  81. data/test_offline.html +0 -189
  82. data/web/assets/css/styles.css +0 -635
  83. data/web/assets/js/app.js +0 -6
  84. data/web/assets/js/bootstrap.js +0 -20
  85. data/web/assets/js/controllers/connection_controller.js +0 -44
  86. data/web/assets/js/controllers/dashboard_controller.js +0 -499
  87. data/web/assets/js/controllers/queue_controller.js +0 -17
  88. data/web/assets/js/controllers/theme_controller.js +0 -31
  89. data/web/assets/js/offline-manager.js +0 -233
  90. data/web/assets/js/service-worker-manager.js +0 -65
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#e3e9f3" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#1a2233" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></g><path fill="currentColor" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="currentColor" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="currentColor"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#e3e9f3" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="#e3e9f3" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#1a2233" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="#1a2233" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><g transform="translate(-13.03 -60.26) scale(1.2414)"><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path class="f" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path class="f" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><g transform="translate(-13.03 -60.26) scale(1.2414)"><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path class="f" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="currentColor"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="#e3e9f3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="#1a2233" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
@@ -27,6 +27,7 @@ end
27
27
  | `connection_name` | gem-generated | Shown in RabbitMQ management UI — set to your service name |
28
28
  | `recovery_attempts` | `10` | Max automatic reconnects; `nil` for infinite |
29
29
  | `recover_from_connection_close` | `true` | Auto-recover after a clean close |
30
+ | `delay_queue_expires` | `3_600_000` | Milliseconds an unused delay queue is kept beyond its delay (`x-expires`); `nil` disables. See [Delayed messages](producers.md#delayed-messages) |
30
31
 
31
32
  ### Named connections
32
33
 
data/docs/consumers.md CHANGED
@@ -42,8 +42,9 @@ configure(
42
42
  | `exchange` | Bind to this exchange. String, or hash (`name`, `type`, `durable`, `auto_delete`). |
43
43
  | `routing_key` | Binding routing key(s). |
44
44
  | `prefetch` | Channel QoS (`basic.qos`). Leave at `1` unless you know you need more. |
45
- | `retry_queue` | When truthy, sets up a retry queue with a delay exchange. Messages requeued via `:requeue` or `max_retry` go here. |
45
+ | `retry_queue` | When truthy, declares a `<queue>.retry` queue and dead-letters rejected messages into it; after `delay` ms they return to the queue. Only `:reject` (and a `perform` that raises, which rejects) goes there; `:requeue` and `:nack` put the message straight back on the queue. |
46
46
  | `error_queue` | Queue name (string) or `true` to auto-name. Messages that exhaust retries land here. |
47
+ | `delays` | Only delays (ms) accepted by `delay!`; anything else raises `Lepus::Error`. See [Delaying a message](#delaying-a-message). |
47
48
  | `process.name` | The named worker pool this consumer runs in — see `Lepus.configure.worker(:name)`. |
48
49
  | `process.threads` | Threads inside the consumer's worker dedicated to this consumer. |
49
50
 
@@ -69,6 +70,7 @@ Return a disposition symbol or call one of the helpers:
69
70
  | `:reject` | `reject!` | Reject without requeue (drops, or goes to DLX if configured). |
70
71
  | `:requeue` | `requeue!` | Reject with requeue — back of the queue. |
71
72
  | `:nack` | `nack!` | Negative-ack. |
73
+ | `:ack` | `delay!(ms)` | Re-deliver the message to this queue after `ms` milliseconds, then ack it. See [Delaying a message](#delaying-a-message). |
72
74
 
73
75
  A `perform` that returns nothing implicitly acks. A `perform` that raises is logged, `on_thread_error` fires, and the message is rejected.
74
76
 
@@ -123,10 +125,42 @@ use :max_retry, retries: 5, error_queue: 'orders.error'
123
125
 
124
126
  On rejection:
125
127
 
126
- 1. `max_retry` inspects `x-death` headers.
127
- 2. If retries are below the limit, it re-publishes to the retry queue.
128
- 3. After TTL, the retry queue dead-letters back to the original queue for another attempt.
129
- 4. After N failures, the middleware routes to the error queue.
128
+ 1. The rejected message is dead-lettered into the retry queue.
129
+ 2. After the TTL, the retry queue dead-letters it back to the original queue for another attempt.
130
+ 3. On each delivery `max_retry` counts the `rejected` entries of the `x-death` header.
131
+ 4. Once the rejected count exceeds `retries` (the first attempt plus N retries), the middleware publishes the payload, without headers or properties, to the error queue and acks the message.
132
+
133
+ ## Delaying a message
134
+
135
+ `delay!(ms)` (aliased `delay`) puts the message being processed back on this consumer's queue after `ms` milliseconds and acks the current delivery. Use it when the work can't be done yet, e.g. waiting for a replica to catch up:
136
+
137
+ ```ruby
138
+ class ReindexConsumer < Lepus::Consumer
139
+ configure(queue: 'reindex', exchange: 'stories', routing_key: 'story.published')
140
+ use :json
141
+
142
+ MAX_DELAYS = 3
143
+
144
+ def perform(message)
145
+ attempts = message.metadata.headers.to_h['x-lepus-delayed-count'].to_i
146
+ return delay!(5_000) if replica_behind? && attempts < MAX_DELAYS
147
+
148
+ reindex(message.payload)
149
+ ack!
150
+ end
151
+ end
152
+ ```
153
+
154
+ It accepts milliseconds or anything responding to `in_milliseconds` (`5.seconds`). No configuration is needed, but every distinct delay creates a queue, so you can list the allowed ones with `configure(..., delays: [5_000, 60_000])`. Any other value raises `Lepus::Error`, which rejects the message like any other error in `perform`.
155
+
156
+ - The message is re-published as received (raw payload, headers and properties, before middlewares changed it), minus `expiration`, through the default exchange to a `reindex.delay.5000` queue with `x-message-ttl: 5000` that dead-letters into `reindex`. The `.delay.` infix keeps it apart from the `<exchange>.delayed.<ms>` queues of producer delays, even when the queue and exchange share a name. Only this queue gets it again, not other queues bound to the exchange.
157
+ - The `x-lepus-delayed-count` header is incremented on each delay, so you can cap your own backoff.
158
+ - The redelivery arrives with routing key `reindex` (the queue name), like retries do. Put anything you route on in the payload or headers.
159
+ - The queue is declared on first use and gets `x-expires` like producer delays; see [Queue expiry](producers.md#queue-expiry).
160
+ - `max_retry` ignores delays: it only counts `rejected` deaths, while delay queues produce `expired` ones.
161
+ - `delay!` returns `:ack`, so the `:unique` middleware releases the dedupe lock under the default `release_on: [:ack]`. Duplicates published during the delay are not suppressed, and the redelivered message doesn't hold a lock.
162
+
163
+ `delay!` must be called from `perform`; outside a delivery it raises `Lepus::Error`. Any publish to the default exchange can be delayed the same way: `Lepus::Publisher.new('').publish(payload, routing_key: 'reindex', delay: 5_000)`.
130
164
 
131
165
  ## Worker assignment
132
166
 
@@ -57,7 +57,7 @@ The `perform` method returns a symbol indicating disposition:
57
57
  - `:ack` — acknowledge; RabbitMQ removes the message.
58
58
  - `:reject` — reject; message is dropped (or routed to a dead-letter exchange if configured).
59
59
  - `:requeue` — reject and requeue for another delivery attempt.
60
- - `:nack` — negative-ack without requeue (similar to `:reject`).
60
+ - `:nack` — negative-ack and requeue (like `:requeue`).
61
61
 
62
62
  `ack!`, `reject!`, `requeue!`, `nack!` are also available as helper methods.
63
63
 
Binary file
data/docs/middleware.md CHANGED
@@ -77,7 +77,7 @@ Track retry count via RabbitMQ `x-death` headers and route to an error queue aft
77
77
  use :max_retry, retries: 5, error_queue: 'orders.error'
78
78
  ```
79
79
 
80
- Requires the consumer's `configure` to declare a `retry_queue:` so requeued messages loop through a delay queue.
80
+ Requires the consumer's `configure` to declare a `retry_queue:` so rejected messages (`:reject` or a raised error) loop through the retry queue.
81
81
 
82
82
  ### `:exception_logger`
83
83
 
data/docs/producers.md CHANGED
@@ -40,6 +40,7 @@ configure(
40
40
  | `exchange` | String (name only) or hash (`name`, `type`, `durable`, `auto_delete`). Auto-declared on first publish. |
41
41
  | `publish` | Default publish options merged into every `publish` call: `persistent`, `mandatory`, `content_type`, `expiration`, etc. |
42
42
  | `connection` | Named connection to publish through. Defaults to `:default` (`rabbitmq_url`). See [Named connections](#named-connections). |
43
+ | `delays` | Only delays (ms) accepted by `publish(..., delay:)`; anything else raises `Lepus::Error`. See [Delayed messages](#delayed-messages). |
43
44
 
44
45
  ## Publishing
45
46
 
@@ -60,6 +61,46 @@ All Bunny publish options are supported. Frequently useful:
60
61
  | `content_type` | MIME type. Auto-set to `application/json` by `:json` middleware. |
61
62
  | `expiration` | Per-message TTL in milliseconds. |
62
63
  | `mandatory` | Return the message if it can't be routed. |
64
+ | `delay` | Deliver the message after this many milliseconds (Integer, Float or anything responding to `in_milliseconds`). Lepus option, never sent to Bunny. See [Delayed messages](#delayed-messages). |
65
+
66
+ ## Delayed messages
67
+
68
+ ```ruby
69
+ OrdersProducer.publish(payload, routing_key: 'order.reminder', delay: 30_000)
70
+ OrdersProducer.publish(payload, routing_key: 'order.reminder', delay: 5.minutes) # ActiveSupport::Duration
71
+ Lepus::Publisher.new('orders').publish(payload, routing_key: 'order.reminder', delay: 30_000)
72
+ ```
73
+
74
+ A default can be set with `configure(publish: { delay: 1_000 })`. Middlewares run as usual and don't see `delay` among the publish options.
75
+
76
+ ### How it works
77
+
78
+ RabbitMQ has no delayed delivery without the `rabbitmq_delayed_message_exchange` plugin, so Lepus builds it from TTL queues and dead-lettering. For a target exchange `orders` and a delay of 30000 ms, Lepus declares on first use:
79
+
80
+ | Resource | Details |
81
+ |----------|---------|
82
+ | `orders.delayed` | Durable headers exchange. Delayed messages are published here with the original routing key plus the `lepus-delay: 30000` header. |
83
+ | `orders.delayed.30000` | Durable queue bound to `orders.delayed` with `x-match: all, lepus-delay: 30000`. Arguments: `x-message-ttl: 30000`, `x-dead-letter-exchange: orders`, `x-expires` (see below). |
84
+
85
+ When the TTL expires RabbitMQ dead-letters the message into `orders`. There's no `x-dead-letter-routing-key`, so it keeps its original routing key and headers, which works for topic, direct, fanout and headers exchanges. The delivered message also carries the `lepus-delay` header and RabbitMQ's `x-death` header.
86
+
87
+ The delay header has no `x-` prefix on purpose: with `x-match: all`, RabbitMQ ignores `x-` binding arguments, so every delay queue would receive every message.
88
+
89
+ Each delay value has its own queue with a per-queue TTL, so there's no head-of-line blocking: a 1 second delay published after a 1 hour delay still arrives after 1 second. Every distinct value creates a queue, so prefer a small set of fixed delays (e.g. 1s, 30s, 5min) over computed ones. Configure the producer with `delays: [1_000, 30_000, 300_000]` to reject any other value with `Lepus::Error` before publishing; Lepus also logs at info level the first time each process declares a delay queue.
90
+
91
+ Lepus doesn't use per-message `expiration` for delays, but one you pass is still applied inside the delay queue: RabbitMQ uses the lower of the two TTLs, so `expiration: 10_000, delay: 60_000` arrives after 10 seconds, and it drops `expiration` when dead-lettering, so the delivered message no longer has it.
92
+
93
+ ### Queue expiry
94
+
95
+ Delay queues get `x-expires` set to the delay plus `config.delay_queue_expires` (default 1 hour), so queues for delays you no longer use are deleted by RabbitMQ, together with any message still in them. Publishing doesn't count as queue usage, so while a process keeps publishing a delay, Lepus re-declares its queue at least every `delay_queue_expires / 2` (and at least once a minute). A message published at any time therefore expires before its queue does. Set `config.delay_queue_expires = nil` to keep delay queues forever.
96
+
97
+ Queue arguments can't change once a queue exists. If you change `delay_queue_expires`, publishing to an existing delay queue raises `Lepus::Error` naming the queue. Keep the previous value, or delete the queue (it only holds messages still waiting) and restart every process that publishes delays in the same deploy: until a process re-declares a deleted queue (up to a minute later), its delayed messages for that queue are dropped.
98
+
99
+ ### Declarations
100
+
101
+ Declarations are memoized per connection, delay queue and arguments, and run on a short-lived channel, so a failure doesn't close the channel used for publishing.
102
+
103
+ `mandatory` and publisher confirms only cover the publish into the delay exchange or queue. Dead-lettering into the target when the TTL expires is not subject to them: if the target exchange doesn't exist, or no binding matches the routing key at that moment, RabbitMQ drops the message without a return or nack. Publishing through the producer declares the exchange, but the bindings must also be in place when the delay ends.
63
104
 
64
105
  ## Payload types
65
106
 
data/docs/testing.md CHANGED
@@ -9,14 +9,18 @@ Lepus ships a test-mode module that captures publishes and runs consumer `perfor
9
9
  require 'lepus/testing'
10
10
 
11
11
  RSpec.configure do |config|
12
- config.before(:each) { Lepus::Testing.enable! }
13
- config.after(:each) { Lepus::Testing.reset! }
12
+ config.before(:each) do
13
+ Lepus::Testing.enable!
14
+ Lepus::Testing.clear_all_messages!
15
+ end
16
+ config.after(:each) { Lepus::Testing.disable! }
14
17
  end
15
18
  ```
16
19
 
17
20
  Once enabled:
18
21
 
19
- - Publishes don't hit RabbitMQ. They're captured in an in-memory buffer keyed by producer class.
22
+ - Publishes don't hit RabbitMQ. They're captured in in-memory fake exchanges keyed by exchange name.
23
+ - Consumer exceptions are re-raised instead of turning into `:reject`, so failures show up in your specs. Use `Lepus::Testing.consumer_capture_errors!` to get the production behavior back.
20
24
  - Consumer handling is synchronous when invoked through `Lepus::Testing.consumer_perform`.
21
25
 
22
26
  ## Testing a consumer
@@ -24,10 +28,7 @@ Once enabled:
24
28
  ```ruby
25
29
  describe OrdersConsumer do
26
30
  it 'creates an order' do
27
- result = Lepus::Testing.consumer_perform(
28
- OrdersConsumer,
29
- { order_id: 42, total: 99.99 }
30
- )
31
+ result = Lepus::Testing.consumer_perform(OrdersConsumer, { order_id: 42, total: 99.99 })
31
32
 
32
33
  expect(result).to eq(:ack)
33
34
  expect(Order.find(42).total).to eq(99.99)
@@ -41,27 +42,24 @@ describe OrdersConsumer do
41
42
  it 'sets delivery info and metadata' do
42
43
  result = Lepus::Testing.consumer_perform(
43
44
  OrdersConsumer,
44
- { order_id: 7 },
45
- delivery_info: { routing_key: 'order.created' },
46
- metadata: { correlation_id: 'abc-123' }
45
+ payload: { order_id: 7 },
46
+ routing_key: 'order.created',
47
+ correlation_id: 'abc-123',
48
+ headers: { 'tenant' => 'acme' }
47
49
  )
48
50
  expect(result).to eq(:ack)
49
51
  end
50
52
  end
51
53
  ```
52
54
 
53
- `consumer_perform` signature:
55
+ `consumer_perform(ConsumerClass, message_or_payload)` accepts:
54
56
 
55
- ```ruby
56
- Lepus::Testing.consumer_perform(
57
- ConsumerClass,
58
- payload,
59
- delivery_info: {},
60
- metadata: {}
61
- )
62
- ```
57
+ - a Hash, used as a JSON payload;
58
+ - a String, used as a `text/plain` payload;
59
+ - a Hash with a `:payload` key, where every other key maps to a `MessageBuilder#with_<key>` method (`routing_key`, `exchange`, `headers`, `correlation_id`, `content_type`, `redelivered`, …);
60
+ - a `Lepus::Message`, e.g. built with `Lepus::Testing.message_builder`.
63
61
 
64
- It builds a `Lepus::Message`, runs the full middleware chain (including global middlewares), and returns the disposition symbol.
62
+ It runs the full middleware chain (including global middlewares) and returns the disposition symbol.
65
63
 
66
64
  ## Testing a producer
67
65
 
@@ -71,23 +69,21 @@ describe OrdersProducer do
71
69
  order = Order.create!(id: 42, total: 99.99)
72
70
  OrdersProducer.order_created(order)
73
71
 
74
- messages = Lepus::Testing.producer_messages(OrdersProducer)
72
+ messages = OrdersProducer.messages # same as Lepus::Testing.producer_messages(OrdersProducer)
75
73
  expect(messages.size).to eq(1)
76
- expect(messages[0][:payload]).to include(order_id: 42)
77
74
  expect(messages[0][:routing_key]).to eq('order.created')
78
75
  end
79
-
80
- it 'runs through middleware' do
81
- OrdersProducer.publish({ foo: 'bar' }, routing_key: 'x')
82
-
83
- msg = Lepus::Testing.producer_messages(OrdersProducer).last
84
- expect(msg[:metadata][:correlation_id]).to be_present # set by :correlation_id middleware
85
- expect(msg[:metadata][:content_type]).to eq('application/json')
86
- end
87
76
  end
88
77
  ```
89
78
 
90
- `producer_messages(ProducerClass)` returns an array of hashes with `:payload`, `:routing_key`, `:delivery_info`, `:metadata`.
79
+ Each captured message is a hash with `:exchange`, `:connection`, `:payload`, `:routing_key`, `:headers`, `:persistent`, `:mandatory`, `:immediate`, `:content_type`, `:delay`, `:expiration` and `:timestamp` (when it was captured). `:payload` is what reached the publisher, so with the `:json` middleware it's the serialized JSON string.
80
+
81
+ Query a fake exchange directly:
82
+
83
+ ```ruby
84
+ Lepus::Testing.exchange('orders').messages
85
+ Lepus::Testing.exchange('orders').find_messages(routing_key: 'order.created', headers: { 'tenant' => 'acme' })
86
+ ```
91
87
 
92
88
  Each fake message also records the named connection it was published through:
93
89
 
@@ -97,22 +93,51 @@ Lepus::Testing.exchange('orders').find_messages(connection: :replica)
97
93
 
98
94
  The connection may be given as a symbol or a string. Publishing through a connection that was never registered with `config.connection` raises `Lepus::InvalidConnectionError`, as it does outside of fake mode.
99
95
 
100
- ## RSpec matchers
96
+ ### Delayed messages
97
+
98
+ `delay:` is recorded on the target exchange, normalized to milliseconds, so `OrdersProducer.messages` and `to_exchange` keep working:
101
99
 
102
100
  ```ruby
103
- # spec/spec_helper.rb
104
- require 'lepus/testing/rspec_matchers'
101
+ OrdersProducer.publish({ order_id: 42 }, routing_key: 'order.reminder', delay: 30_000)
102
+
103
+ expect(OrdersProducer.messages.last[:delay]).to eq(30_000)
105
104
  ```
106
105
 
107
- Then:
106
+ A consumer's `delay!(ms)` is recorded on the default exchange (`''`) with the consumer queue as routing key, and the `x-lepus-delayed-count` header incremented:
107
+
108
+ ```ruby
109
+ Lepus::Testing.consumer_perform(ReindexConsumer, { story_id: 1 })
110
+
111
+ message = Lepus::Testing.exchange('').messages.last
112
+ expect(message).to include(routing_key: 'reindex', delay: 5_000)
113
+ ```
114
+
115
+ ## RSpec matchers
116
+
117
+ The matchers are included in RSpec when `lepus/testing` is required.
108
118
 
109
119
  ```ruby
110
120
  expect { OrdersProducer.order_created(order) }
111
- .to have_published_message(OrdersProducer)
112
- .with_payload(include(order_id: order.id))
113
- .to_routing_key('order.created')
121
+ .to lepus_publish_message
122
+ .to_exchange('orders')
123
+ .with_routing_key('order.created')
124
+ .with(satisfy { |json| MultiJson.load(json)['order_id'] == order.id }) # :json middleware serialized it
125
+
126
+ expect { OrdersProducer.publish(payload, delay: 30_000) }
127
+ .to lepus_publish_message(1).with_delay(30_000)
128
+
129
+ # Matches the messages already captured for a producer
130
+ expect(OrdersProducer).to lepus_publish_message.with_routing_key('order.created')
131
+
132
+ # Consumer dispositions
133
+ expect(OrdersConsumer).to lepus_acknowledge_message({ order_id: 42 }) # alias: lepus_ack_message
134
+ expect(OrdersConsumer).to lepus_reject_message({ bad: 'data' })
135
+ expect(OrdersConsumer).to lepus_requeue_message(payload)
136
+ expect(OrdersConsumer).to lepus_nack_message(payload)
114
137
  ```
115
138
 
139
+ `with_delay` accepts milliseconds or anything responding to `in_milliseconds`. `with` takes a value or any RSpec matcher and is checked against the captured `:payload`.
140
+
116
141
  ## Testing middleware in isolation
117
142
 
118
143
  Middlewares are plain Ruby objects with a `call(message, app)` method. Unit-test them directly:
@@ -121,8 +146,8 @@ Middlewares are plain Ruby objects with a `call(message, app)` method. Unit-test
121
146
  describe LogLevelMiddleware do
122
147
  it 'logs before calling down the chain' do
123
148
  middleware = LogLevelMiddleware.new(level: :debug)
124
- message = Lepus::Testing::MessageBuilder.build(payload: { x: 1 })
125
- captured = nil
149
+ message = Lepus::Testing.message_builder.with_payload({ x: 1 }).with_routing_key('order.created').build
150
+ captured = nil
126
151
 
127
152
  allow(Lepus.logger).to receive(:debug) { |msg| captured = msg }
128
153
  middleware.call(message, ->(m) { :ack })
@@ -132,17 +157,11 @@ describe LogLevelMiddleware do
132
157
  end
133
158
  ```
134
159
 
135
- `Lepus::Testing::MessageBuilder.build(**kwargs)` builds a realistic `Lepus::Message` for unit tests.
160
+ `Lepus::Testing.message_builder` returns a `Lepus::Testing::MessageBuilder`; chain `with_*` methods (`with_payload`, `with_routing_key`, `with_headers`, `with_correlation_id`, `with_channel`, …) and call `build` to get a `Lepus::Message`.
136
161
 
137
162
  ## Resetting between tests
138
163
 
139
- `Lepus::Testing.reset!` clears captured publishes. In shared setup:
140
-
141
- ```ruby
142
- RSpec.configure do |config|
143
- config.before(:each) { Lepus::Testing.enable!; Lepus::Testing.reset! }
144
- end
145
- ```
164
+ `Lepus::Testing.clear_all_messages!` clears captured publishes. `Lepus::Testing.disable!` turns fake publishing and error re-raising off.
146
165
 
147
166
  ## When you do need a real RabbitMQ
148
167
 
data/docs/web.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Lepus web dashboard](images/lepus-web.png)
4
4
 
5
- Lepus ships a Rack-based monitoring UI showing consumer status, throughput, and recent activity.
5
+ Lepus ships a Rack-based monitoring UI: a live map of how messages move from exchanges through queues to consumers, the broker's message rates, and the supervisors, workers, queues and connections behind them.
6
6
 
7
7
  ## Running it standalone
8
8
 
@@ -19,7 +19,7 @@ Visit http://localhost:9292.
19
19
  require 'lepus/web'
20
20
 
21
21
  authenticate :user, ->(u) { u.admin? } do
22
- mount Lepus::Web::App, at: '/lepus'
22
+ mount Lepus::Web => '/lepus'
23
23
  end
24
24
  ```
25
25
 
@@ -27,7 +27,7 @@ Or with Devise:
27
27
 
28
28
  ```ruby
29
29
  authenticate :admin_user do
30
- mount Lepus::Web::App, at: '/admin/lepus'
30
+ mount Lepus::Web => '/admin/lepus'
31
31
  end
32
32
  ```
33
33
 
@@ -35,11 +35,34 @@ end
35
35
 
36
36
  ## What it shows
37
37
 
38
- - **Supervisors.** Every running `lepus start` process.
39
- - **Workers.** Subprocesses per supervisor, with their named pool and PID.
40
- - **Consumers.** Per-consumer message counts (processed, rejected, errored), queue names, routing keys.
41
- - **Exchanges & queues.** RabbitMQ topology as seen by the gem.
42
- - **Recent activity.** Last N messages — timestamps, routing keys, dispositions.
38
+ - **Topology map.** Each exchange is a band of lines, one per bound queue, running exchange → queue → consumer → acknowledged. Amber tokens ride each segment at a pace that grows with its message rate (on a log scale, capped), a hollow amber bar is a backlog piling up at its queue, violet loops under a line are messages waiting in the retry queue and red bars count messages parked in the error queue. Messages delayed on purpose (see below) wait in a teal clock pill tucked under the queue they will be released to, or under the exchange name when they are released to the exchange; they are never counted as backlog or retries. Quiet queues, and anything past the busiest few per exchange, fold into a "+N" row you can expand. Point at or focus a queue for exact numbers; select it to find it in the queues table.
39
+ - **Headline.** Messages flowing, queues backing up (growing, or with nobody reading them), messages retrying, messages delayed (when there are any) and messages in error queues, in one sentence that also serves as the map's key.
40
+ - **Message rates.** Published, delivered, acknowledged and redelivered per second across the broker over the last 10 minutes or hour, from the samples the management API keeps, with a table view.
41
+ - **Queues.** Every main queue under the exchange that routes to it, in the map's order, with each exchange's totals; a queue bound to several exchanges sits under the first and notes the others, and queues with no binding come last under "Default exchange", as on the map. Columns are ready (with how fast a growing backlog grows), unacked, consumers, published, delivered and acknowledged per second (on an exchange's row, Published/s is what reaches the exchange), and the messages in the queue's retry and error queues ("–" when it has none). Sorting orders the queues within each exchange. Expand a queue to see its retry, delay and error queues with their own counts and what their arguments do ("retries after 5 s, back to orders.billing", "releases after 5 s to orders.billing, expires when unused for 1 h"). A queue's row says how many messages wait in its delay queues and for how long ("340 delayed for 1 min and 1 h"); an exchange's delay queues are listed right under the exchange, and its totals include everything delayed for it; delay queues whose queue or exchange no longer exists are listed under "Default exchange". Idle queues are muted, and an exchange's idle queues fold into one row; on a broker with more than 30 queues each exchange shows its first six and folds the rest into "+N more queues". Selecting a queue on the map brings its row into view, unfolded, expanded and focused; a queue's name leads back to it on the map.
42
+ - **Processes.** Supervisors, their workers (PID, memory, connections, heartbeat health) and each consumer's processed, rejected and errored counts since the worker started. A consumer configured with `delays:` lists them, and messages it handed back with `delay!` count as delayed rather than processed.
43
+ - **Connections.** The broker's client connections.
44
+
45
+ Retry and error queues are recognised from what consumers report in their heartbeats, from their arguments (a TTL queue that dead-letters into a queue which dead-letters back is its retry queue; a queue another one dead-letters into is its error queue) and from the `.retry` / `.error` suffixes. A queue a consumer reads, or one bound to a named exchange, is always drawn as a main queue.
46
+
47
+ Delay queues are the ones Lepus declares for [delayed messages](../README.md#delayed-messages), recognised by their name and arguments together: `<queue>.delay.<ms>` with an `x-message-ttl` of `<ms>` dead-letters into `<queue>` through the default exchange (a `delay:` publish through the default exchange, or a consumer's `delay!`), and `<exchange>.delayed.<ms>` with an `x-message-ttl` of `<ms>` dead-letters into `<exchange>`, keeping the routing key, behind the `<exchange>.delayed` headers exchange (a `delay:` publish to any other exchange). The `<exchange>.delayed` exchange draws no band of its own. A delay queue RabbitMQ deleted after `x-expires` (`config.delay_queue_expires` past its delay) simply drops off the next poll.
48
+
49
+ ## Refreshing
50
+
51
+ The page polls every 5 seconds by default. The **Live** control in the top bar pauses it or switches to 2, 5, 15 or 60 seconds, and the choice is remembered in the browser. Polling also stops while the tab is in the background, and tokens freeze while paused. The light/dark theme follows the system unless you pick one.
52
+
53
+ The page loads nothing from other origins (its font is bundled), uses no inline scripts or styles and is served with a strict `Content-Security-Policy`. Earlier versions installed a service worker for offline use; it now removes itself and its caches on the next visit.
54
+
55
+ ## Trying it without RabbitMQ
56
+
57
+ From a checkout of the gem:
58
+
59
+ ```bash
60
+ bin/web-demo # http://127.0.0.1:9393
61
+ HEAVY=1 bin/web-demo # ~70 queues on 12 exchanges, 20 workers, deep backlogs
62
+ MOUNT=/lepus bin/web-demo # served under a sub-path, like a Rails mount
63
+ ```
64
+
65
+ It simulates a broker whose rates drift, with a backlog that builds and drains and a burst of errors every few minutes.
43
66
 
44
67
  ## Registry backend
45
68
 
@@ -66,13 +89,16 @@ Each worker heartbeats into the registry every `config.process_heartbeat_interva
66
89
 
67
90
  ## API endpoints
68
91
 
69
- The dashboard exposes a minimal read-only JSON API at `/api/...` — the UI is a single-page app that consumes it. You can also consume the API directly for custom dashboards or alerting.
92
+ The dashboard is a single page reading a small read-only JSON API under `/api/...`. You can use it directly for custom dashboards or alerting:
70
93
 
71
- Endpoints include (subject to change):
94
+ - `GET /api/topology`: exchanges, bindings (with their routing keys), queues with their role (`main`, `retry`, `error`, `delay`) and parent, rates and backlog, message TTL, dead-letter exchange and routing key and `x-expires`, and the consumers of each queue (with their `delayed` count and `delays`) merged from the broker and worker heartbeats. A delay queue also has `delay_ms`, `target` and `target_type` (`queue` or `exchange`); every queue and exchange has `delayed`, the messages waiting to be released to it, and `delays`, the delays they wait; a `<exchange>.delayed` exchange has role `delay` and `releases_to`, and bindings carry the `delays` of their `lepus-delay` header. `management` is `false` (with `management_error`) when the management API did not answer; the topology is then what consumers declared.
95
+ - `GET /api/overview?age=600|3600`: broker-wide message rates, queue totals and rate history over the window.
96
+ - `GET /api/processes`: every process sending heartbeats, with per-consumer counts.
97
+ - `GET /api/queues`, `GET /api/exchanges`, `GET /api/connections`: the management API's lists.
98
+ - `GET /api/meta`: version, heartbeat settings and polling choices.
99
+ - `GET /api/health`
72
100
 
73
- - `GET /api/processes` — all tracked processes
74
- - `GET /api/consumers` — all registered consumers with counts
75
- - `GET /api/queues` — queue metadata from the RabbitMQ management API (if `management_api_url` is set)
101
+ `/api/processes`, `/api/meta` and `/api/health` work without the RabbitMQ Management API. `/api/topology` degrades to what worker heartbeats declared when it is missing, and the rest need it (`config.management_api_url`, default `http://<rabbitmq host>:15672`, credentials and vhost taken from `rabbitmq_url`). When it fails, the dashboard only says whether it was unreachable, rejected the credentials or returned an error; the details go to the log.
76
102
 
77
103
  ## Prometheus metrics
78
104
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lepus (0.2.2)
4
+ lepus (0.3.0)
5
5
  base64
6
6
  bunny
7
7
  concurrent-ruby
@@ -160,7 +160,7 @@ GEM
160
160
  thor (>= 0.19.0, < 2.0)
161
161
  rainbow (3.1.1)
162
162
  rake (13.4.2)
163
- rbtree (0.4.6)
163
+ rbtree (0.4.7)
164
164
  redis (5.4.1)
165
165
  redis-client (>= 0.22.0)
166
166
  redis-client (0.28.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lepus (0.2.2)
4
+ lepus (0.3.0)
5
5
  base64
6
6
  bunny
7
7
  concurrent-ruby
@@ -78,8 +78,8 @@ GEM
78
78
  base64 (0.3.0)
79
79
  bigdecimal (4.1.1)
80
80
  builder (3.3.0)
81
- bunny (3.1.0)
82
- amq-protocol (~> 2.7)
81
+ bunny (3.2.0)
82
+ amq-protocol (~> 2.8)
83
83
  logger (~> 1, >= 1.7)
84
84
  sorted_set (~> 1, >= 1.0.2)
85
85
  coderay (1.1.3)
@@ -179,7 +179,7 @@ GEM
179
179
  thor (~> 1.0)
180
180
  rainbow (3.1.1)
181
181
  rake (13.4.2)
182
- rbtree (0.4.6)
182
+ rbtree (0.4.7)
183
183
  redis (5.4.1)
184
184
  redis-client (>= 0.22.0)
185
185
  redis-client (0.28.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lepus (0.2.2)
4
+ lepus (0.3.0)
5
5
  base64
6
6
  bunny
7
7
  concurrent-ruby