{"id":368166,"date":"2024-05-21T03:36:53","date_gmt":"2024-05-21T03:36:53","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=368166"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=368166","title":{"rendered":"<span>\u041a\u0430\u043a \u0441\u043e\u0437\u0434\u0430\u0442\u044c YouTube GIF Maker \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c Next.js, Node \u0438 RabbitMQ<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u043d\u0430 <a href=\"https:\/\/github.com\/ragrag\/youtube-gif\"><u>github<\/u><\/a>, \u0430 <a href=\"http:\/\/ytgif.vercel.app\/\"><u>\u043f\u043e \u044d\u0442\u043e\u0439 \u0441\u0441\u044b\u043b\u043a\u0435<\/u><\/a> \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0434\u0435\u043c\u043e-\u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\u00a0<\/p>\n<h2>\u041a\u0440\u0430\u0442\u043a\u0438\u0439 \u043e\u0431\u0437\u043e\u0440<\/h2>\n<h4>\u0418\u0434\u0435\u044f<\/h4>\n<p>\u0418\u0434\u0435\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u2014 \u0434\u0430\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c GIF-\u0444\u0430\u0439\u043b\u044b \u0438\u0437 \u0432\u0438\u0434\u0435\u043e\u0440\u043e\u043b\u0438\u043a\u043e\u0432 \u0441 YouTube. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u043d\u0443\u0436\u043d\u043e\u0435 \u0432\u0438\u0434\u0435\u043e, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430\u0434\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0438 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f GIF.<\/p>\n<h4>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 GIF \u0438\u0437 \u0432\u0438\u0434\u0435\u043e \u043d\u0430 YouTube \u0441 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u043e\u043c.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435\u043c \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u044b<\/h3>\n<h4>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0441\u0438\u0441\u0442\u0435\u043c\u044b<\/h4>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0440\u0430\u0431\u043e\u0442\u0443 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u043c\u0438.<\/p>\n<ul>\n<li>\n<p>\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043d\u0430 React (Next.js). \u041e\u0431\u0449\u0430\u0435\u0442\u0441\u044f \u0441 \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u0443\u0436\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044f\u0445.<\/p>\n<\/li>\n<li>\n<p>\u0411\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430 Node. \u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u043e\u0432\u044b\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e \u0432 \u0431\u0440\u043e\u043a\u0435\u0440 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p>Node \u043a\u0430\u043a Service Worker. \u0411\u0443\u0434\u0435\u0442 \u043e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u0437\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \/ \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0437\u0430\u0434\u0430\u0447 \u043f\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u2014 RabbitMQ. \u0411\u0443\u0434\u0435\u0442 \u0432\u044b\u0441\u0442\u0443\u043f\u0430\u0442\u044c \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447 (Task Queue), \u0432 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0438, \u0430 Service Worker \u2014 \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u044f\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043d\u0435\u0451.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 MongoDB. \u0411\u0443\u0434\u0435\u0442 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0437\u0430\u0434\u0430\u0447\u0430\u0445 \u043f\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 GIF.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430\u0444\u0430\u0439\u043b\u043e\u0432 \u2014 Google Cloud Storage. \u0411\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 GIF-\u0444\u0430\u0439\u043b\u043e\u0432.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430<\/h4>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c0c\/b79\/2dd\/c0cb792dd1b48c95f3be5f6323f995e5.png\" alt=\"System Architecture\" title=\"\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u044b\" width=\"880\" height=\"711\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c0c\/b79\/2dd\/c0cb792dd1b48c95f3be5f6323f995e5.png\"\/><\/p>\n<div><figcaption>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u044b<\/figcaption><\/div>\n<\/figure>\n<h4>\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438<\/h4>\n<p>\u042d\u0442\u043e \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043f\u043e\u0442\u043e\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0449\u0438\u0439 \u0432\u0441\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b, \u0443\u043f\u043e\u043c\u044f\u043d\u0443\u0442\u044b\u0435 \u0432\u044b\u0448\u0435.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/148\/21a\/3f4\/14821a3f4548bdd64e5890a61a7795de.png\" alt=\"Sequence Diagram\" title=\"\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438\" width=\"880\" height=\"512\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/148\/21a\/3f4\/14821a3f4548bdd64e5890a61a7795de.png\"\/><\/p>\n<div><figcaption>\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438<\/figcaption><\/div>\n<\/figure>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440.<\/p>\n<hr\/>\n<h2>\u0411\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440<\/h2>\n<h3>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438<\/h3>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cc3\/c1d\/0ca\/cc3c1d0ca655d662b084fe9fb21021e4.png\" alt=\"backend_sequence\" title=\"\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439\" width=\"880\" height=\"512\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/cc3\/c1d\/0ca\/cc3c1d0ca655d662b084fe9fb21021e4.png\"\/><\/p>\n<div><figcaption>\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439<\/figcaption><\/div>\n<\/figure>\n<p>\u041a\u0430\u043a \u0432\u0438\u0434\u043d\u043e \u0438\u0437 \u043f\u0440\u0438\u0432\u0435\u0434\u0451\u043d\u043d\u043e\u0439 \u0432\u044b\u0448\u0435 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b, \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u0442\u0440\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438:<\/p>\n<ul>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF \u043f\u0443\u0442\u0451\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 RabbitMQ, \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u0445, \u0447\u0442\u043e \u0431\u044b\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0430 \u043d\u043e\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e (\u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0437\u0430\u0434\u0430\u0447 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u044c).<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447 \u043f\u0443\u0442\u0451\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e \u0435\u0451 ID \u0438\u0437 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h3>\n<p>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u0442\u0440\u0451\u0445 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ul>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440<\/p>\n<\/li>\n<li>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0438\u0437 \u043d\u0438\u0445 \u043e\u0431\u043b\u0430\u0434\u0430\u0435\u0442 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u043e\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435; \u0430 \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043c\u0441\u044f, \u043f\u043e\u0447\u0435\u043c\u0443 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/4c5\/f17\/bd0\/4c5f17bd07ae0f0dc50255881cddfc7f.png\" alt=\"Backend Architecture\" title=\"\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0431\u044d\u043a\u0435\u043d\u0434\u0430\" width=\"880\" height=\"277\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/4c5\/f17\/bd0\/4c5f17bd07ae0f0dc50255881cddfc7f.png\"\/><\/p>\n<div><figcaption>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0431\u044d\u043a\u0435\u043d\u0434\u0430<\/figcaption><\/div>\n<\/figure>\n<h4>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432<\/h4>\n<ul>\n<li>\n<p>\u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\u043c \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432. \u041e\u0431\u044b\u0447\u043d\u043e \u044d\u0442\u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 \u043c\u0430\u0441\u0441\u0438\u0432\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c &#171;Middleware Chain&#187;, \u0430 \u043a\u043e\u043d\u0435\u0447\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u043c \u0432 \u044d\u0442\u043e\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430 (Route Controller).<\/p>\n<\/li>\n<li>\n<p>Middleware Chain \u043e\u0431\u044b\u0447\u043d\u043e \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u00ab\u043f\u0440\u043e\u0432\u0435\u0440\u043e\u043a\u00bb \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0432 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u0437\u0430 \u043c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u0412 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0433\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430-\u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u0430.<\/p>\n<\/li>\n<\/ul>\n<h4>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440<\/h4>\n<ul>\n<li>\n<p>\u0418\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0447\u0438\u0449\u0430\u0435\u0442 \u043f\u0440\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0435\u043b\u0435\u0433\u0438\u0440\u0443\u0435\u0442 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u043c\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u0443.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u044b.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0438 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u043e\u043c\u0443 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043e\u043a.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0421\u0435\u0440\u0432\u0438\u0441<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0432\u0441\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443.<\/p>\n<\/li>\n<li>\n<p>\u0418\u043c\u0435\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u0430\u043d\u043d\u044b\u043c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0440\u043e\u0432\u043d\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0434\u0430\u043d\u043d\u044b\u043c (ORM\/ODM).<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c <em>\u0433\u043b\u0443\u043f\u044b\u043c\u0438<\/em>, \u0442\u043e \u0435\u0441\u0442\u044c \u043d\u0435 \u0438\u043c\u0435\u0442\u044c \u043d\u0438\u043a\u0430\u043a\u0438\u0445 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0435\u0439 \u043e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0435. \u0412\u0441\u0451, \u0447\u0442\u043e \u043e\u043d\u0438 \u0437\u043d\u0430\u044e\u0442, \u044d\u0442\u043e \u00ab\u043a\u0430\u043a\u043e\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u00bb, \u00ab\u043a\u0430\u043a\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0443\u0436\u043d\u044b \u044d\u0442\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u0443\u00bb, \u00ab\u043a\u0430\u043a \u0434\u043e\u043b\u0436\u0435\u043d \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u043e\u0442\u0432\u0435\u0442\u00bb. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043d\u0430\u043b\u0438\u0447\u0438\u044f \u0442\u043e\u043b\u0441\u0442\u044b\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u0432 (Fat Controllers).<\/p>\n<h3>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f<\/h3>\n<h4>\u0421\u0445\u0435\u043c\u0430 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445<\/h4>\n<p>\u0412 \u044d\u0442\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c <a href=\"https:\/\/typeorm.io\/#\/\"><u>TypeORM<\/u><\/a>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 ORM \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 TypeScript \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445 (\u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c MongoDB).<\/p>\n<p>\u041c\u044b \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043c \u043a\u0430\u0436\u0434\u0443\u044e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF \u0432 \u0432\u0438\u0434\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 (Job), \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0448\u0435\u0439 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0435\u0439 (Collection).<\/p>\n<p>Job Collection \u0432 TypeORM \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">import { BaseEntity, Entity, ObjectID, Column, CreateDateColumn, UpdateDateColumn, ObjectIdColumn } from 'typeorm';  @Entity('jobs') export class Job extends BaseEntity {   @ObjectIdColumn()   id: ObjectID;    @Column({     nullable: false,   })   youtubeUrl: string;    @Column({     nullable: false,   })   youtubeId: string;    @Column({     nullable: true,   })   gifUrl: string;    @Column({     nullable: false,   })   startTime: number;    @Column({     nullable: false,   })   endTime: number;    @Column({     type: 'enum',     enum: ['pending', 'processing', 'done', 'error'],   })   status: 'pending' | 'processing' | 'done' | 'error';    @Column()   @CreateDateColumn()   createdAt: Date;    @Column()   @UpdateDateColumn()   updatedAt: Date; }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0432\u0430\u0436\u043d\u043e \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043f\u043e\u043b\u0435 <code>status<\/code> \u2014 \u043f\u043e \u0441\u0443\u0442\u0438, \u043e\u043d\u043e \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435\u043c (enum), \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u043c \u043d\u0430 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 GIF. \u0412\u0441\u0435 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u043e\u043b\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442 \u0441\u043e\u0431\u043e\u0439 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<h4>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432<\/h4>\n<p>\u0423 \u043d\u0430\u0441 \u0431\u0443\u0434\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0432\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430.<\/p>\n<ul>\n<li>\n<p>\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 GIF.<\/p>\n<\/li>\n<li>\n<p>\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u043e \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0437 \u0435\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u043c \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043d\u0430\u0448 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432:<\/p>\n<pre><code class=\"typescript\">\/\/routes.interface import { Router } from 'express';  interface Route {   path?: string;   router: Router; }  export default Route;<\/code><\/pre>\n<pre><code class=\"typescript\">\/\/jobs.route.ts import { Router } from 'express'; import { CreateJobDto } from '..\/..\/common\/dtos\/createJob.dto'; import Route from '..\/..\/common\/interfaces\/routes.interface'; import JobsController from '..\/..\/controllers\/jobs.controller'; import validationMiddleware from '..\/middlewares\/validation.middleware';  class JobsRoute implements Route {   public path = '\/jobs';   public router = Router();    constructor(private jobsController = new JobsController()) {     this.initializeRoutes();   }    private initializeRoutes() {     this.router.get(`${this.path}\/:id`, this.jobsController.getJobById);     this.router.post(`${this.path}`, validationMiddleware(CreateJobDto, 'body'), this.jobsController.createJob);   } }  export default JobsRoute;<\/code><\/pre>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0441\u0432\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a-\u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 DTO \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/github.com\/typestack\/class-validator\"><u>class-validator<\/u><\/a> \u0438 <a href=\"https:\/\/github.com\/typestack\/class-transformer\"><u>class-transformer<\/u><\/a><\/p>\n<pre><code class=\"typescript\">\/\/createJob.dto import { Expose } from 'class-transformer'; import { IsNotEmpty, IsNumber, IsString, Matches } from 'class-validator'; import { IsGreaterThan } from '.\/validators\/isGreaterThan'; import { MaximumDifference } from '.\/validators\/maximumDifference';  export class CreateJobDto {   @IsNotEmpty()   @IsString()   @Matches(\/^(?:https?:\\\/\\\/)?(?:www\\.)?(?:youtu\\.be\\\/|youtube\\.com\\\/(?:embed\\\/|v\\\/|watch\\?v=|watch\\?.+&amp;v=))((\\w|-){11})(?:\\S+)?$\/, {     message: 'Invalid youtube url',   })   @Expose()   public youtubeUrl: string;    @IsNotEmpty()   @IsNumber()   @Expose()   public startTime: number;    @IsNotEmpty()   @IsNumber()   @IsGreaterThan('startTime', {     message: 'end time must be greater than start time',   })   @MaximumDifference('startTime', {     message: 'maximum gif duration is 30 seconds',   })   @Expose()   public endTime: number; }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e <code>IsGreaterThan<\/code> \u0438 <code>MaximumDifference<\/code> \u2014 \u044d\u0442\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u044b \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0434\u043b\u044f <code>class-validator<\/code>. \u041f\u043e \u0441\u0443\u0442\u0438, \u043e\u043d\u0438 \u0432\u044b\u0433\u043b\u044f\u0434\u044f\u0442 \u0442\u0430\u043a (\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0447\u0438\u0442\u0430\u0439\u0442\u0435 \u0432 <a href=\"https:\/\/github.com\/typestack\/class-validator#custom-validation-decorators\"><u>\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438<\/u><\/a>):<\/p>\n<pre><code class=\"typescript\">\/\/isGreaterThan.ts import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';  export function IsGreaterThan(property: string, validationOptions?: ValidationOptions) {   return function (object: Object, propertyName: string) {     registerDecorator({       name: 'isGreaterThan',       target: object.constructor,       propertyName: propertyName,       constraints: [property],       options: validationOptions,       validator: {         validate(value: any, args: ValidationArguments) {           const [relatedPropertyName] = args.constraints;           const relatedValue = (args.object as any)[relatedPropertyName];           return typeof value === 'number' &amp;&amp; typeof relatedValue === 'number' &amp;&amp; value > relatedValue;          },       },     });   }; }<\/code><\/pre>\n<p><code>MaximumDifference<\/code> \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e, \u043d\u043e \u0435\u0433\u043e \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">return typeof value === 'number' &amp;&amp; typeof relatedValue === 'number' &amp;&amp; value - relatedValue &lt;= difference; <\/code><\/pre>\n<p>\u0418 \u0442\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u0448 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a (validation middleware) \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">validation.middleware.ts import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; import { RequestHandler } from 'express';  const validationMiddleware = (type: any, value: string | 'body' | 'query' | 'params' = 'body', skipMissingProperties = false): RequestHandler => {   return (req, res, next) => {     validate(plainToClass(type, req[value]), { skipMissingProperties }).then((errors: ValidationError[]) => {       if (errors.length > 0) {         const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', ');         res.status(400).send(message);       } else {         next();       }     });   }; };  export default validationMiddleware; <\/code><\/pre>\n<h4>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440<\/h4>\n<p>\u041d\u0430\u0448 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e. \u0415\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435, \u0447\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u2014 \u044d\u0442\u043e \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 <code>CreateJobDto<\/code> \u0438\u0437 \u0442\u0435\u043b\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <code>plainToClass<\/code> \u0438\u0437 <code>class-transformer<\/code> \u0441 <code>excludeExtraneousValues: true<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0443\u043d\u0438\u0447\u0442\u043e\u0436\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044f (\u0441 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u043c <code><a class=\"mention\" href=\"\/users\/Expose\">@Expose<\/a>()<\/code> \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 <code>CreateJobDto<\/code>) \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c <a href=\"https:\/\/github.com\/typestack\/class-transformer#plaintoclass\"><u>\u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043f\u043e class-transformer.<\/u><\/a><\/p>\n<pre><code class=\"typescript\">\/\/jobs.controllers.ts import { plainToClass } from 'class-transformer'; import { NextFunction, Request, Response } from 'express'; import { CreateJobDto } from '..\/common\/dtos\/createJob.dto'; import { Job } from '..\/entities\/jobs.entity'; import JobsService from '..\/services\/jobs.service';  class JobsController {   constructor(private jobService = new JobsService()) {}    public createJob = async (req: Request, res: Response, next: NextFunction): Promise&lt;void> => {     try {       const jobDto: CreateJobDto = plainToClass(CreateJobDto, req.body, { excludeExtraneousValues: true });       const createdJob: Job = await this.jobService.createJob(jobDto);        res.status(201).json(createdJob);     } catch (error) {       next(error);     }   };    public getJobById = async (req: Request, res: Response, next: NextFunction): Promise&lt;void> => {     try {       const jobId = req.params.id;       const job: Job = await this.jobService.findJobById(jobId);        const responseStatus = job.status === 'done' ? 200 : 202;       res.status(responseStatus).json(job);     } catch (error) {       next(error);     }   }; }  export default JobsController;<\/code><\/pre>\n<p>\u0422\u0430\u043a\u0436\u0435 \u0441\u0442\u043e\u0438\u0442 \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c, \u0447\u0442\u043e \u043a\u043e\u0434 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0442\u0432\u0435\u0442\u0430 [GET] \/job\/{id} \u0440\u0430\u0432\u0435\u043d 202, \u0435\u0441\u043b\u0438 \u0437\u0430\u0434\u0430\u0447\u0430 \u043d\u0430 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0441\u0451 \u0435\u0449\u0451 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c \u0447\u0438\u0442\u0430\u0439\u0442\u0435 \u0432 \u043f\u043e\u0441\u0442\u0435 <a href=\"https:\/\/dev.to\/ragrag\/asynchronous-request-response-pattern-2pbj\"><u>\u00ab\u0410\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u044b\u0439 \u0448\u0430\u0431\u043b\u043e\u043d \u0437\u0430\u043f\u0440\u043e\u0441\u0430-\u043e\u0442\u0432\u0435\u0442\u0430\u00bb<\/u><\/a>.<\/p>\n<p>\u0412 \u0441\u043b\u0443\u0447\u0430\u0435 \u0432\u043e\u0437\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u044f \u043e\u0448\u0438\u0431\u043a\u0438 \u043e\u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0432 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043e\u0448\u0438\u0431\u043e\u043a (error middleware), \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u043c \u043f\u043e\u0441\u0440\u0435\u0434\u043d\u0438\u043a\u043e\u043c \u0432 \u043d\u0430\u0448\u0435\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0435 \u0438 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">\/\/error.middleware.ts import { NextFunction, Request, Response } from 'express';  import { isBoom, Boom } from '@hapi\/boom'; import { logger } from '..\/..\/common\/utils\/logger';  function errorMiddleware(error: Boom | Error, req: Request, res: Response, next: NextFunction) {   const statusCode: number = isBoom(error) ? error.output.statusCode : 500;   const errorMessage: string = isBoom(error) ? error.message : 'Something went wrong';   logger.error(`StatusCode : ${statusCode}, Message : ${error}`);    return res.status(statusCode).send(errorMessage); } export default errorMiddleware;<\/code><\/pre>\n<p>\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0430\u043c\u0435\u0442\u0438\u0442\u044c, \u0447\u0442\u043e \u043c\u044b \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0438 \u043f\u0430\u043a\u0435\u0442 \u043f\u043e\u0434 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c Boom \u2014 \u043e \u043d\u0451\u043c \u043c\u044b \u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c \u043f\u043e\u0437\u0436\u0435 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u0421\u0435\u0440\u0432\u0438\u0441\u044b\u00bb.<\/p>\n<h4>\u0421\u0435\u0440\u0432\u0438\u0441\u044b<\/h4>\n<h4>\u0421\u043b\u0443\u0436\u0431\u0430 \u0437\u0430\u0434\u0430\u0447 (jobs.service)<\/h4>\n<p><code>JobService<\/code> \u0438\u043c\u0435\u0435\u0442 \u0432\u0441\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443 \u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u043b\u043e\u044e Data Access, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442 \u0441 RabbitMQ Service \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u044c.<\/p>\n<pre><code class=\"typescript\">\/\/jobs.service.ts import * as Boom from '@hapi\/boom'; import Container from 'typedi'; import { CreateJobDto } from '..\/common\/dtos\/createJob.dto'; import EventEmitter from '..\/common\/utils\/eventEmitter'; import { Job } from '..\/entities\/jobs.entity'; import RabbitMQService from '.\/rabbitmq.service';  class JobsService {   private events = {     JobCreated: 'JobCreated',   };    constructor() {     this.intiializeEvents();   }    private intiializeEvents() {     EventEmitter.on(this.events.JobCreated, (job: Job) => {       const rabbitMQInstance = Container.get(RabbitMQService);       rabbitMQInstance.sendToQueue(JSON.stringify(job));     });   }    public async findJobById(jobId: string): Promise&lt;Job> {     const job: Job = await Job.findOne(jobId);     if (!job) throw Boom.notFound();      return job;   }    public async createJob(jobDto: CreateJobDto): Promise&lt;Job> {     const createdJob: Job = await Job.save({ ...jobDto, youtubeId: jobDto.youtubeUrl.split('v=')[1]?.slice(0, 11), status: 'pending' } as Job);     EventEmitter.emit(this.events.JobCreated, createdJob);     return createdJob;   } }  export default JobsService;<\/code><\/pre>\n<p>\u0421\u0440\u0430\u0437\u0443 \u0436\u0435 \u0432\u044b \u0443\u0432\u0438\u0434\u0438\u0442\u0435 \u0434\u0432\u0430 \u0438\u043c\u043f\u043e\u0440\u0442\u0430, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0431\u044b\u0442\u044c \u043d\u0435\u0437\u043d\u0430\u043a\u043e\u043c\u044b \u2014 \u043c\u044b \u0431\u044b\u0441\u0442\u0440\u043e \u043f\u0440\u043e\u0439\u0434\u0451\u043c\u0441\u044f \u043f\u043e \u043d\u0438\u043c, \u0430 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u043e\u0431\u044a\u044f\u0441\u043d\u0438\u043c \u043a\u0430\u0436\u0434\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0432 \u044d\u0442\u043e\u043c \u043a\u043b\u0430\u0441\u0441\u0435.<\/p>\n<ul>\n<li>\n<p><a href=\"https:\/\/hapi.dev\/module\/boom\/\"><u>Boom<\/u><\/a><u><br \/><\/u>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f http-\u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0441 \u043c\u043e\u0449\u043d\u044b\u043c, \u043f\u0440\u043e\u0441\u0442\u044b\u043c \u0438 \u0434\u0440\u0443\u0436\u0435\u043b\u044e\u0431\u043d\u044b\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u043c.\u00a0<\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/typestack\/typedi\"><u>TypeDI<\/u><\/a><u><br \/><\/u>TypeDI \u2014 \u044d\u0442\u043e \u043c\u043e\u0449\u043d\u044b\u0439 \u043f\u0430\u043a\u0435\u0442 \u0434\u043b\u044f \u0438\u043d\u044a\u0435\u043a\u0446\u0438\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043e\u0431\u043b\u0430\u0434\u0430\u0435\u0442 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e\u043c \u0444\u0438\u0447\u0435\u0439. \u041e\u0434\u043d\u043e\u0439 \u0438\u0437 \u0442\u0430\u043a\u0438\u0445 \u0444\u0438\u0447 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u043b\u0438\u0447\u0438\u0435 Singleton Services, \u0447\u0442\u043e \u043c\u044b \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0432 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435.<\/p>\n<\/li>\n<\/ul>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0432 \u043a\u043b\u0430\u0441\u0441\u0435.<\/p>\n<h4>intiializeEvents()<\/h4>\n<p>\u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0439 <code>EventEmitter<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0432\u043e \u0432\u0441\u0451\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u043b\u043e\u0439 pub\/sub.<\/p>\n<p>\u044d\u0442\u043e \u0442\u0430\u043a \u0436\u0435 \u043f\u0440\u043e\u0441\u0442\u043e, \u043a\u0430\u043a<\/p>\n<pre><code class=\"typescript\">\/\/eventEmitter.ts import { EventEmitter } from 'events'; export default new EventEmitter();<\/code><\/pre>\n<p>\u0438 \u0442\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043d\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u0412 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438, \u0441\u043e\u0431\u044b\u0442\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043c\u044b \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u043e\u0437\u0436\u0435 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e\u0434 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c &#8216;JobCreated&#8217;<\/p>\n<pre><code class=\"typescript\">  \/\/ Defines all the events in our service   private events = {     JobCreated: 'JobCreated',   };    private intiializeEvents() {     \/\/ Start listening for the event 'JobCreated'     EventEmitter.on(this.events.JobCreated, (job: Job) => {     \/\/ Get a singleton instance of our RabbitMQService       const rabbitMQInstance = Container.get(RabbitMQService);     \/\/ Dispatch an event containing the data of the created job       rabbitMQInstance.sendToQueue(JSON.stringify(job));     });   }<\/code><\/pre>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0447\u0438\u0442\u0430\u0439\u0442\u0435 \u0432 \u043f\u043e\u0441\u0442\u0435 <a href=\"https:\/\/dev.to\/ragrag\/adding-a-pub-sub-layer-to-your-express-backend-3p09\"><u>\u00ab\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043b\u043e\u044f Pub\/Sub \u0432 Express \u0431\u044d\u043a\u0435\u043d\u0434\u00bb.<\/u><\/a><\/p>\n<h4>createJob()<\/h4>\n<p>\u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f:<\/p>\n<ul>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0451\u0442 \u043d\u043e\u0432\u044b\u0439 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 &#8216;JobCreated&#8217; \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u0431\u044b\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0430 \u043d\u043e\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0441\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u044f (event listener) \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0432 \u0441\u043b\u0443\u0436\u0431\u0443 RabbitMQ.<\/p>\n<\/li>\n<\/ul>\n<h4>RabbitMQ Service<\/h4>\n<p>\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0438\u0441 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 RabbitMQ, \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043a\u0430\u043d\u0430\u043b\u0430 \u0438 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u0438, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u0447 (\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u044f\u0442\u044c\u0441\u044f \u043d\u0430\u0448\u0438\u043c Service Worker-\u043e\u043c).<\/p>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0434\u043b\u044f RabbitMQ Server \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <a href=\"https:\/\/github.com\/squaremo\/amqp.node\"><u>amqplib<\/u><\/a><u>.<\/u><\/p>\n<pre><code class=\"typescript\">\/\/rabbitmq.service.ts import { Service } from 'typedi'; import amqp, { Channel, Connection } from 'amqplib'; import { logger } from '..\/common\/utils\/logger';  @Service() export default class RabbitMQService {   private connection: Connection;   private channel: Channel;   private queueName = 'ytgif-jobs';   constructor() {     this.initializeService();   }    private async initializeService() {     try {       await this.initializeConnection();       await this.initializeChannel();       await this.initializeQueues();     } catch (err) {       logger.error(err);     }   }   private async initializeConnection() {     try {       this.connection = await amqp.connect(process.env.NODE_ENV === 'production' ? process.env.RABBITMQ_PROD : process.env.RABBITMQ_DEV);       logger.info('Connected to RabbitMQ Server');     } catch (err) {       throw err;     }   }    private async initializeChannel() {     try {       this.channel = await this.connection.createChannel();       logger.info('Created RabbitMQ Channel');     } catch (err) {       throw err;     }   }    private async initializeQueues() {     try {       await this.channel.assertQueue(this.queueName, {         durable: true,       });       logger.info('Initialized RabbitMQ Queues');     } catch (err) {       throw err;     }   }    public async sendToQueue(message: string) {     this.channel.sendToQueue(this.queueName, Buffer.from(message), {       persistent: true,     });     logger.info(`sent: ${message} to queue ${this.queueName}`);   } }<\/code><\/pre>\n<p>\u041a\u043e\u0434 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \/ \u043a\u0430\u043d\u0430\u043b\u043e\u0432 \/ \u043e\u0447\u0435\u0440\u0435\u0434\u0435\u0439 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0435\u043d. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u0438\u0442\u044c\u0441\u044f \u0441 \u044d\u0442\u0438\u043c\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c\u0438 \u043c\u043e\u0436\u043d\u043e \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 <a href=\"https:\/\/www.rabbitmq.com\/tutorials\/tutorial-two-javascript.html\"><u>RabbitMQ<\/u><\/a> \u0438\u043b\u0438 <a href=\"http:\/\/www.squaremobius.net\/amqp.node\/channel_api.html\"><u>anqplib<\/u><\/a>. \u0415\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u0430\u044f \u0444\u0443\u043d\u043a\u0446\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043d\u0435 \u044d\u0442\u043e\u0433\u043e \u043a\u043b\u0430\u0441\u0441\u0430 \u2014 \u044d\u0442\u043e <code>sendToQueue()<\/code>. \u041e\u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 \u043d\u0430\u0448\u0443 \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u0437\u0430\u0434\u0430\u0447, \u043a\u0430\u043a \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0432 JobService, \u043f\u0443\u0442\u0451\u043c \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 <code>Job<\/code>.<\/p>\n<pre><code class=\"typescript\"> rabbitMQInstance.sendToQueue(JSON.stringify(job));<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c RabbitMQ Service \u0432 \u043d\u0430\u0447\u0430\u043b\u0435 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">import Container from 'typedi';  \/\/ Call initializeRabbitMQ() somewhere when starting the app private initializeRabbitMQ() {     Container.get(RabbitMqService);   }<\/code><\/pre>\n<p>\u0420\u0430\u0431\u043e\u0442\u0430 \u043d\u0430\u0448\u0435\u0433\u043e \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041e\u0441\u0442\u0430\u043b\u043e\u0441\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e, \u0447\u0442\u043e\u0431\u044b Node Service Worker \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u0437\u0430\u0434\u0430\u0447 \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u043b \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 GIF. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c.<\/p>\n<hr\/>\n<h2>Node Service Worker\u00a0<\/h2>\n<h3>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438<\/h3>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c71\/988\/176\/c71988176a370131f67944227cf485ec.png\" alt=\"Worker General Sequence Diagram\" width=\"880\" height=\"512\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c71\/988\/176\/c71988176a370131f67944227cf485ec.png\"\/><\/p>\n<div><figcaption>Worker General Sequence Diagram<\/figcaption><\/div>\n<\/figure>\n<p>\u041a\u0430\u043a \u0432\u0438\u0434\u0438\u0442\u0435, Service Worker \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430:<\/p>\n<ul>\n<li>\n<p>\u041f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447 \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447;<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0447\u0430\u0441\u0442\u0438 \u0432\u0438\u0434\u0435\u043e \u043d\u0430 YouTube \u0432 GIF;<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0443 GIF \u0432 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435;<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 gifUrl \u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0411\u043b\u043e\u043a-\u0441\u0445\u0435\u043c\u0430<\/h3>\n<p>\u042d\u0442\u0430 \u0431\u043b\u043e\u043a-\u0441\u0445\u0435\u043c\u0430 \u043d\u0430\u0433\u043b\u044f\u0434\u043d\u043e \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0443 Service Worker-\u0430.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c7e\/18d\/740\/c7e18d740f2dfc52c01af757015d7fe1.png\" alt=\"Worker Flow Chart\" title=\"\u0411\u043b\u043e\u043a-\u0441\u0445\u0435\u043c\u0430 \u0440\u0430\u0431\u043e\u0442\u044b Service Worker-\u0430 \" width=\"880\" height=\"286\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c7e\/18d\/740\/c7e18d740f2dfc52c01af757015d7fe1.png\"\/><\/p>\n<div><figcaption>\u0411\u043b\u043e\u043a-\u0441\u0445\u0435\u043c\u0430 \u0440\u0430\u0431\u043e\u0442\u044b Service Worker-\u0430 <\/figcaption><\/div>\n<\/figure>\n<h3>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f<\/h3>\n<h4>RabbitMQ Service<\/h4>\n<h4>\u041f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447 \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438<\/h4>\n<p>RabbitMQ Service \u0432 Service Worker-\u0435 \u043f\u043e\u0445\u043e\u0436 \u043d\u0430 RabbitMQ Service \u043d\u0430 \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440\u0435, \u0437\u0430 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0439 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u2014 <code>startConsuming()<\/code><\/p>\n<pre><code class=\"typescript\">\/\/rabbitmq.service.ts import amqp, { Channel, Connection, ConsumeMessage } from 'amqplib'; import Container, { Service } from 'typedi'; import { Job } from '..\/entities\/jobs.entity'; import ConversionService from '.\/conversion.service';  @Service() export default class RabbitMQService {   private connection: Connection;   private channel: Channel;   private queueName = 'ytgif-jobs';   constructor() {     this.initializeService();   }    private async initializeService() {     try {       await this.initializeConnection();       await this.initializeChannel();       await this.initializeQueues();       await this.startConsuming();     } catch (err) {       console.error(err);     }   }   private async initializeConnection() {     try {       this.connection = await amqp.connect(process.env.NODE_ENV === 'production' ? process.env.RABBITMQ_PROD : process.env.RABBITMQ_DEV);       console.info('Connected to RabbitMQ Server');     } catch (err) {       throw err;     }   }    private async initializeChannel() {     try {       this.channel = await this.connection.createChannel();       console.info('Created RabbitMQ Channel');     } catch (err) {       throw err;     }   }    private async initializeQueues() {     try {       await this.channel.assertQueue(this.queueName, {         durable: true,       });       console.info('Initialized RabbitMQ Queues');     } catch (err) {       throw err;     }   }    public async startConsuming() {     const conversionService = Container.get(ConversionService);     this.channel.prefetch(1);     console.info(' ? Waiting for messages in %s. To exit press CTRL+C', this.queueName);     this.channel.consume(       this.queueName,       async (msg: ConsumeMessage | null) => {         if (msg) {           const job: Job = JSON.parse(msg.content.toString());           console.info(`Received new job ? `, job.id);           try {             await conversionService.beginConversion(               job,               () => {                 this.channel.ack(msg);               },               () => {                 this.channel.reject(msg, false);               },             );           } catch (err) {             console.error('Failed to process job', job.id, err);           }         }       },       {         noAck: false,       },     );   } }<\/code><\/pre>\n<p><code>startConsuming()<\/code> \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u044f\u0435\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438, \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u0435\u0433\u043e JSON-\u043e\u0431\u044a\u0435\u043a\u0442 \u0438 \u0434\u0435\u043b\u0435\u0433\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 \u0432 <code>ConversionService<\/code>.<\/p>\n<p>\u0412\u0441\u0451, \u0447\u0442\u043e \u043d\u0443\u0436\u043d\u043e <code>ConversionService<\/code> \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f, \u2014 \u044d\u0442\u043e \u043e\u0431\u044a\u0435\u043a\u0442 <code>Job<\/code>, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u0432\u0430 \u043a\u043e\u043b\u043b\u0431\u044d\u043a\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0445 \u0434\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 (\u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0438\u0436\u0435).<\/p>\n<p>\u0422\u0430\u043a\u0436\u0435 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432 \u044d\u0442\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c<\/p>\n<pre><code class=\"typescript\">this.channel.prefetch(1);<\/code><\/pre>\n<p>\u041e\u0431 \u044d\u0442\u043e\u043c \u043c\u044b \u0442\u0430\u043a\u0436\u0435 \u043f\u043e\u0433\u043e\u0432\u043e\u0440\u0438\u043c \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u043f\u043e\u0437\u0436\u0435.<\/p>\n<h4>\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f<\/h4>\n<p>\u0427\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0443 \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 (\u0447\u0442\u043e \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0443\u0435\u0442 \u043e \u0442\u043e\u043c, \u0447\u0442\u043e \u0441\u0435\u0440\u0432\u0438\u0441 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0437\u0430\u0434\u0430\u0447\u0443 \u043b\u0438\u0431\u043e \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043b\u0438\u0431\u043e \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e), \u043d\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0440\u0443\u0447\u043d\u043e\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435.<\/p>\n<p>\u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0432 amqplib, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043b\u0438\u0431\u043e<\/p>\n<pre><code class=\"typescript\">channel.ack(msg);<\/code><\/pre>\n<p>\u0427\u0442\u043e\u0431\u044b \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f.<\/p>\n<p>\u041b\u0438\u0431\u043e<\/p>\n<pre><code class=\"typescript\">\/\/ \u0412\u0442\u043e\u0440\u043e\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442, \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u043b\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u0438\u043b\u0438 \u043d\u0435\u0442 channel.reject(msg, false);<\/code><\/pre>\n<p>\u0447\u0442\u043e\u0431\u044b \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f.<\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0448\u0438\u0431\u043a\u0438 \u043c\u044b \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u044c, \u0430 \u0441\u0447\u0438\u0442\u0430\u0435\u043c \u0435\u0433\u043e \u00ab\u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u043c \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c\u00bb. \u041d\u043e \u044d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430.<\/p>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u0432 RabbitMQ \u043c\u043e\u0436\u043d\u043e <a href=\"https:\/\/www.rabbitmq.com\/confirms.html\"><u>\u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c.<\/u><\/a><\/p>\n<h4>\u0421\u0435\u0440\u0432\u0438\u0441 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f<\/h4>\n<p>\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0438\u0441 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043e\u0441\u043d\u043e\u0432\u043d\u0443\u044e \u043b\u043e\u0433\u0438\u043a\u0443 \u043d\u0430\u0448\u0435\u0433\u043e Service Worker-\u0430.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/429\/9cc\/8c5\/4299cc8c56fa8d6a23c6e1c4cbe22658.png\" alt=\"Conversion Service\" width=\"880\" height=\"466\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/429\/9cc\/8c5\/4299cc8c56fa8d6a23c6e1c4cbe22658.png\"\/><\/p>\n<div><figcaption>Conversion Service<\/figcaption><\/div>\n<\/figure>\n<p>\u041e\u043d \u0440\u0430\u0441\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u044e <code>beginConversion()<\/code>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u0441\u043b\u0443\u0436\u0431\u044b RabbitMQ \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f<\/p>\n<pre><code class=\"typescript\">public async beginConversion(job: Job, { onSuccess, onError }: { onSuccess: () => void; onError: () => void }) { ... }<\/code><\/pre>\n<p>\u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442 \u0432\u0441\u0435 \u0448\u0430\u0433\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432\u044b\u0437\u043e\u0432\u0435\u0442 \u043b\u0438\u0431\u043e <code>onSuccess()<\/code>, \u043b\u0438\u0431\u043e <code>onError()<\/code> \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0443\u0441\u043f\u0435\u0445\u0430 \u0438\u043b\u0438 \u043d\u0435\u0443\u0434\u0430\u0447\u0438.<\/p>\n<p>\u0428\u0430\u0433\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u0438\u0434\u0435\u043e \u0441 YouTube \u0432 GIF:<\/p>\n<ul>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0432\u0438\u0434\u0435\u043e \u0441 YouTube.<\/p>\n<ul>\n<li>\n<p>\u0412\u0438\u0434\u0435\u043e \u0441 YouTube \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e.<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ul>\n<li>\n<p>\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0438\u0434\u0435\u043e \u0432 GIF.<\/p>\n<ul>\n<li>\n<p>\u0412\u0438\u0434\u0435\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0432 GIF (\u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043d\u0430\u0447\u0430\u043b\u0430\/\u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f).<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<ul>\n<li>\n<p>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 GIF \u0432 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 Google.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<li>\n<p>\u0412\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f onSuccess() \u0438\u043b\u0438 onError() \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e.<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0432\u0438\u0434\u0435\u043e \u0441 YouTube.<\/p>\n<h4>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0432\u0438\u0434\u0435\u043e \u0441 YouTube<\/h4>\n<p>\u0427\u0442\u043e\u0431\u044b \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0432\u0438\u0434\u0435\u043e \u0441 YouTube \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e, \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u043f\u0430\u043a\u0435\u0442 <a href=\"https:\/\/github.com\/fent\/node-ytdl-core\"><u>ytdl-core<\/u><\/a>, \u043f\u0440\u0435\u0434\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044b\u0439 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438.<\/p>\n<p>\u0417\u0430 \u044d\u0442\u043e \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u044f<em> <\/em><code>downloadVideo()<\/code>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 URL\/ID \u0432\u0438\u0434\u0435\u043e \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 <a href=\"https:\/\/nodejs.org\/api\/stream.html#stream_readable_streams\"><u>ReadableStream<\/u><\/a>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0432\u0438\u0434\u0435\u043e\u0444\u0430\u0439\u043b\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0435\u0433\u043e \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: mp4, avi&#8230; \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435.<\/p>\n<pre><code class=\"typescript\">\/\/conversion.service.ts import { Readable } from 'stream'; import ytdl from 'ytdl-core'; import YoutubeDownload from '..\/common\/interfaces\/YoutubeDownload';   private async downloadVideo({ youtubeId, youtubeUrl }: YoutubeDownload): Promise&lt;{ video: Readable ; formatExtension: string }> {     const info = await ytdl.getInfo(youtubeId);     const format: ytdl.videoFormat = info.formats[0];     if (!format) throw new Error('No matching format found');     const video = ytdl(youtubeUrl, {       format,     });     return { video, formatExtension: format.container };   }  public async beginConversion(job: Job, { onSuccess, onError }: { onSuccess: () => void; onError: () => void }) {     try {       console.info('Started Processing Job :', job.id);        const { video, formatExtension } = await this.downloadVideo({         youtubeId: job.youtubeId,         youtubeUrl: job.youtubeUrl,       });        const srcFileName = `.\/src\/media\/temp.${formatExtension}`;        video.on('progress', (chunkLength, downloaded, total) => {         \/\/... Logic for showing progress to the user..i.e progress bar       });        video.pipe(         fs           .createWriteStream(srcFileName)           .on('open', () => {             \/\/Video download started             console.log('Downloading Video');           })           .on('finish', async () => {             \/\/Video finished downloading locally in srcFileName             console.info('Downloaded video for job ', job.id);             \/\/...Logic for converting the locally downloaded video to GIF           })           .on('error', async () => {               \/\/...handle failure logic           }),       );     } catch (err) {         \/\/...handle failure logic     }   }<\/code><\/pre>\n<h4>\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0438\u0434\u0435\u043e \u0432 GIF<\/h4>\n<p>\u0414\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0432\u0438\u0434\u0435\u043e \u0432 GIF \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/github.com\/ffmpegwasm\/ffmpeg.wasm\"><u>ffmpeg.wasm<\/u><\/a>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e \u0441\u0443\u0442\u0438 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f Webassembly-\u043f\u043e\u0440\u0442\u043e\u043c FFmpeg. \u042d\u0442\u043e\u0442 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u0431\u0435 \u043a\u0430\u043a \u0430\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u043d\u043e\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 <a href=\"https:\/\/ffmpeg.org\/ffmpeg.html\"><u>FFmpeg<\/u><\/a> \u0432\u043d\u0443\u0442\u0440\u0438 \u043d\u043e\u0434\u044b \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u043f\u043e\u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0439 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0432, \u043d\u0438\u043a\u0430\u043a\u0438\u0445 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0445 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0438 \u0442\u0430\u043a \u0434\u0430\u043b\u0435\u0435 \u2014 \u0447\u0442\u043e \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043e\u0447\u0435\u043d\u044c \u043c\u043e\u0449\u043d\u043e \u0438 \u043f\u0440\u043e\u0441\u0442\u043e.<\/p>\n<pre><code class=\"typescript\">\/\/conversion.service.ts import { createFFmpeg, fetchFile, FFmpeg } from '@ffmpeg\/ffmpeg'; import GifConversion from '..\/common\/interfaces\/GifConversion';  \/\/...somewhere in our code  const ffmpeg = createFFmpeg({         log: false,         progress: p => {           progressBar.update(Math.floor(p.ratio * 100));         },       }); await ffmpeg.load();    \/\/Converts a video range to GIF from srcFileName to destFileName  private async convertToGIF({ startTime, endTime, srcFileName, destFileName, formatExtension }: GifConversion) {     try {       console.info('Converting Video to GIF');       this.ffmpeg.FS('writeFile', `temp.${formatExtension}`, await fetchFile(srcFileName));       await this.ffmpeg.run(         '-i',         `temp.${formatExtension}`,         '-vcodec',         'gif',         '-ss',         `${startTime}`,         '-t',         `${endTime - startTime}`,         '-vf',         'fps=10',         `temp.gif`,       );       await fs.promises.writeFile(destFileName, this.ffmpeg.FS('readFile', 'temp.gif'));       console.info('Converted video to gif');     } catch (err) {       throw err;     }   }  public async beginConversion(job: Job, { onSuccess, onError }: { onSuccess: () => void; onError: () => void }) {     try {       console.info('Started Processing Job :', job.id);        const srcFileName = `.\/src\/media\/temp.${formatExtension}`;       const destFileName = `.\/src\/media\/temp.gif`;        \/\/... Video download logic       \/\/ GIF Conversion       await this.convertToGIF({         startTime: job.startTime,         endTime: job.endTime,         srcFileName,         destFileName,         formatExtension,       });     } catch (err) {         \/\/...handle failure logic     }   }<\/code><\/pre>\n<h4>\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 GIF \u0432 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 Google<\/h4>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0442\u043e\u0433\u043e \u043a\u0430\u043a \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0432\u0438\u0434\u0435\u043e\u0444\u0430\u0439\u043b \u0431\u044b\u043b \u0441\u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d \u0432 GIF, \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0433\u043e \u0432 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 Google.<\/p>\n<p>\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043b\u0443\u0436\u0431\u0443 <code>CloudStorageService<\/code>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u0438\u043c\u0435\u043d\u043d\u043e \u0437\u0430 \u044d\u0442\u043e.<\/p>\n<p>\u0412 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c Google Cloud Storage.<\/p>\n<pre><code class=\"typescript\">import { Storage } from '@google-cloud\/storage'; import * as _ from 'lodash'; import { Service } from 'typedi';  @Service() class CloudStorageService {   private storage;   private BUCKET_NAME;   constructor() {     const privateKey = _.replace(process.env.GCS_PRIVATE_KEY, new RegExp('\\\\\\\\n', 'g'), '\\n');     this.BUCKET_NAME = 'yourbucketname';     this.storage = new Storage({       projectId: process.env.GCS_PROJECT_ID,       credentials: {         private_key: privateKey,         client_email: process.env.GCS_CLIENT_EMAIL,       },     });   }    async uploadGif(gifImage: Buffer, uploadName: string) {     try {       const bucket = await this.storage.bucket(this.BUCKET_NAME);       uploadName = `ytgif\/${uploadName}`;       const file = bucket.file(uploadName);       await file.save(gifImage, {         metadata: { contentType: 'image\/gif' },         public: true,         validation: 'md5',       });       return `https:\/\/storage.googleapis.com\/${this.BUCKET_NAME}\/${uploadName}`;     } catch (err) {       throw new Error('Something went wrong while uploading image');     }   } }  export default CloudStorageService;<\/code><\/pre>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u0434\u043b\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e GIF.<\/p>\n<pre><code class=\"typescript\">\/\/conversion.service.ts import Container from 'typedi'; import CloudStorageService from '.\/cloudStorage.service';  private async uploadGifToCloudStorage(destFileName, uploadName): Promise&lt;string> {     try {       console.info('Uploading gif to cloud storage');       const gifImage = await fs.promises.readFile(destFileName);       const cloudStorageInstance = Container.get(CloudStorageService);       const gifUrl = await cloudStorageInstance.uploadGif(gifImage, `gifs\/${uploadName}`);       return gifUrl;     } catch (err) {       throw err;     }   }  public async beginConversion(job: Job, { onSuccess, onError }: { onSuccess: () => void; onError: () => void }) {     try {       const destFileName = `.\/src\/media\/temp.gif`;        \/\/... Video download logic       \/\/... Video conversion logic       const gifUrl = await this.uploadGifToCloudStorage(destFileName, job.id);      } catch (err) {         \/\/...handle failure logic     }   }<\/code><\/pre>\n<h4>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 success\/failure<\/h4>\n<p>\u041e\u043d\u0430 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u0430. \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0443 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<ul>\n<li>\n<p>\u0412 \u0441\u043b\u0443\u0447\u0430\u0435 \u0443\u0441\u043f\u0435\u0445\u0430:<br \/>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438 \u043d\u0430 &#8216;done&#8217; \u0438 \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 gifUrl \u043d\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 GIF \u0432 Google Cloud Storage.<\/p>\n<\/li>\n<li>\n<p>\u0412 \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0435\u0443\u0434\u0430\u0447\u0438:<br \/>\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438 \u043d\u0430 &#8216;error&#8217;.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u0432\u044b\u0437\u043e\u0432\u0435\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u044e <code>onSuccess()<\/code> \u0438\u043b\u0438 <code>onError()<\/code>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f, \u043f\u043e \u0441\u0443\u0442\u0438, \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \/ \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f RabbitMQ.<\/p>\n<pre><code class=\"typescript\">public async beginConversion(job: Job, { onSuccess, onError }: { onSuccess: () => void; onError: () => void }) {     try {       const destFileName = `.\/src\/media\/temp.gif`;       \/\/... Video download logic       \/\/... Video conversion logic       const gifUrl = await this.uploadGifToCloudStorage(destFileName, job.id);       \/\/Success scenario       await this.jobService.updateJobById(job.id as any, { status: 'done', gifUrl });       console.info(`Finished job ${job.id}, gif at ${gifUrl}`);       onSuccess();     } catch (err) {         \/\/Failure scenario         console.error('Failed to process job', job.id);         await this.jobService.updateJobById(job.id as any, { status: 'error' });         onError();     }   }<\/code><\/pre>\n<h4>\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0432\u0441\u0451 \u0432\u043c\u0435\u0441\u0442\u0435<\/h4>\n<p>\u0415\u0441\u043b\u0438 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0451 \u0432\u043c\u0435\u0441\u0442\u0435, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0448\u043a\u0430\u043b\u0443 \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/github.com\/AndiDittrich\/Node.CLI-Progress\"><u>cli-progress<\/u><\/a>, \u0442\u043e <code>ConversionService<\/code> \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">import Container, { Service } from 'typedi'; import JobsService from '.\/jobs.service'; import ytdl from 'ytdl-core'; import { Readable } from 'stream'; import { Job } from '..\/entities\/jobs.entity'; import { createFFmpeg, fetchFile, FFmpeg } from '@ffmpeg\/ffmpeg'; import fs from 'fs'; import cliProgress from 'cli-progress'; import CloudStorageService from '.\/cloudStorage.service'; import GifConversion from '..\/common\/interfaces\/GifConversion'; import YoutubeDownload from '..\/common\/interfaces\/YoutubeDownload';  const progressBar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);  @Service() export default class ConversionService {   private ffmpeg: FFmpeg = null;    constructor(private jobService = new JobsService()) {}    public async initializeService() {     try {       this.ffmpeg = createFFmpeg({         log: false,         progress: p => {           progressBar.update(Math.floor(p.ratio * 100));         },       });       await this.ffmpeg.load();     } catch (err) {       console.error(err);     }   }    private async downloadVideo({ youtubeId, youtubeUrl }: YoutubeDownload): Promise&lt;{ video: Readable; formatExtension: string }> {     const info = await ytdl.getInfo(youtubeId);     const format: ytdl.videoFormat = info.formats[0];     if (!format) throw new Error('No matching format found');     const video = ytdl(youtubeUrl, {       format,     });     return { video, formatExtension: format.container };   }    private async convertToGIF({ startTime, endTime, srcFileName, destFileName, formatExtension }: GifConversion) {     try {       console.info('Converting Video to GIF');       this.ffmpeg.FS('writeFile', `temp.${formatExtension}`, await fetchFile(srcFileName));       progressBar.start(100, 0);       await this.ffmpeg.run(         '-i',         `temp.${formatExtension}`,         '-vcodec',         'gif',         '-ss',         `${startTime}`,         '-t',         `${endTime - startTime}`,         '-vf',         'fps=10',         `temp.gif`,       );       progressBar.stop();       await fs.promises.writeFile(destFileName, this.ffmpeg.FS('readFile', 'temp.gif'));       console.info('Converted video to gif');     } catch (err) {       throw err;     }   }    private async uploadGifToCloudStorage(destFileName, uploadName): Promise&lt;string> {     try {       console.info('Uploading gif to cloud storage');       const gifImage = await fs.promises.readFile(destFileName);       const cloudStorageInstance = Container.get(CloudStorageService);       const gifUrl = await cloudStorageInstance.uploadGif(gifImage, `gifs\/${uploadName}`);       return gifUrl;     } catch (err) {       throw err;     }   }    public async beginConversion(job: Job, { onSuccess, onError }: { onSuccess: () => void; onError: () => void }) {     try {       await this.jobService.updateJobById(job.id as any, { status: 'processing' });       console.info('Started Processing Job :', job.id);        const { video, formatExtension } = await this.downloadVideo({         youtubeId: job.youtubeId,         youtubeUrl: job.youtubeUrl,       });        const srcFileName = `.\/src\/media\/temp.${formatExtension}`;       const destFileName = `.\/src\/media\/temp.gif`;        video.on('progress', (chunkLength, downloaded, total) => {         let percent: any = downloaded \/ total;         percent = percent * 100;         progressBar.update(percent);       });        video.pipe(         fs           .createWriteStream(srcFileName)           .on('open', () => {             console.log('Downloading Video');             progressBar.start(100, 0);           })           .on('finish', async () => {             progressBar.stop();             console.info('Downloaded video for job ', job.id);              await this.convertToGIF({               startTime: job.startTime,               endTime: job.endTime,               srcFileName,               destFileName,               formatExtension,             });              const gifUrl = await this.uploadGifToCloudStorage(destFileName, job.id);              await this.jobService.updateJobById(job.id as any, { status: 'done', gifUrl });             console.info(`Finished job ${job.id}, gif at ${gifUrl}`);             onSuccess();           })           .on('error', async () => {             progressBar.stop();             console.error('Failed to process job', job.id);             await this.jobService.updateJobById(job.id as any, { status: 'error' });             onError();           }),       );     } catch (err) {       await this.jobService.updateJobById(job.id as any, { status: 'error' });       onError();       throw err;     }   } } <\/code><\/pre>\n<p>\u0412\u0441\u043f\u043e\u043c\u043d\u0438\u0442\u0435, \u043a\u0430\u043a \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0438 <code>channel.prefetch(1)<\/code>, \u043a\u043e\u0433\u0434\u0430 \u043d\u0430\u0447\u0430\u043b\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u0435 \u0438\u0437 \u043e\u0447\u0435\u0440\u0435\u0434\u0438<\/p>\n<pre><code class=\"typescript\">this.channel.prefetch(1);<\/code><\/pre>\n<p>\u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043a\u0430\u0436\u0434\u044b\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044c \u043e\u0447\u0435\u0440\u0435\u0434\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0437\u0430 \u0440\u0430\u0437. \u042d\u0442\u043e \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u0443\u0435\u0442, \u0447\u0442\u043e \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430 \u043c\u0435\u0436\u0434\u0443 \u043d\u0430\u0448\u0438\u043c\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u043c\u0438, \u0438 \u0432\u0441\u044f\u043a\u0438\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044c \u043e\u0441\u0432\u043e\u0431\u043e\u0434\u0438\u0442\u0441\u044f, \u043e\u043d \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c \u0434\u0440\u0443\u0433\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438.<\/p>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 <a href=\"https:\/\/www.rabbitmq.com\/tutorials\/tutorial-two-javascript.html\"><u>RabbitMQ Docs<\/u><\/a>.<\/p>\n<p>\u042d\u0442\u043e \u0442\u0430\u043a\u0436\u0435 \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442, \u0447\u0442\u043e \u0435\u0441\u043b\u0438 \u043c\u044b \u0437\u0430\u0445\u043e\u0442\u0438\u043c \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0438 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e \/ Service Worker-\u044b, \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u043a\u043e\u043f\u0438\u0439 \u044d\u0442\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430.<\/p>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043e \u0432 \u0437\u0430\u043c\u0435\u0442\u043a\u0435 <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/architecture\/patterns\/competing-consumers\"><u>\u00ab\u041a\u043e\u043d\u043a\u0443\u0440\u0438\u0440\u0443\u044e\u0449\u0438\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438\u00bb<\/u><\/a>.<\/p>\n<p>\u0412\u043e\u0442 \u0438 \u0432\u0441\u0451 \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0433\u043e Service Worker-\u0430! \u0422\u0435\u043f\u0435\u0440\u044c \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043d\u0430\u0447\u0430\u0442\u044c \u043a\u043e\u043f\u0430\u0442\u044c\u0441\u044f \u0432 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<p>\u041d\u0430\u043c \u043e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c, \u043a\u0430\u043a \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c Next.js Client, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e \u0438 \u043f\u0440\u043e\u0441\u043c\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044c \u0441\u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 GIF.<\/p>\n<hr\/>\n<h2>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0439 \u0447\u0430\u0441\u0442\u0438<\/h2>\n<h3>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438<\/h3>\n<p>\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043d\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u0430, \u043e\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0434\u0435\u043b\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0432\u0435 \u0432\u0435\u0449\u0438:<\/p>\n<ul>\n<li>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0443 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF \u0438 \u043f\u0440\u043e\u0441\u043c\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044c \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 GIF, \u043a\u043e\u0433\u0434\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0441\u0440\u0430\u0437\u0443 \u043f\u0435\u0440\u0435\u0439\u0434\u0451\u043c \u043a \u043f\u0435\u0440\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0435 \u2014 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.<\/p>\n<h3>\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430<\/h3>\n<p>\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u044d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0438\u043c\u0435\u0442\u044c:<\/p>\n<ul>\n<li>\n<p>\u041f\u043e\u043b\u044f \u0432\u0432\u043e\u0434\u0430, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0435:<\/p>\n<ul>\n<li>\n<p>URL \u0432\u0438\u0434\u0435\u043e\u00a0<\/p>\n<\/li>\n<li>\n<p>\u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 GIF<\/p>\n<\/li>\n<li>\n<p>\u0432\u0440\u0435\u043c\u044f \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f GIF<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u0412\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044c YouTube \u0441 \u043f\u0440\u0435\u0432\u044c\u044e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0432\u0438\u0434\u0435\u043e \u0438 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u043e\u043c \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0432\u0435 \u043a\u043d\u043e\u043f\u043a\u0438: \u0434\u043b\u044f \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0432\u044b\u0431\u043e\u0440\u0430 \u0438 \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0432\u044b\u0431\u043e\u0440\u0430 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f GIF.<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u0440\u0451\u0445 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0445 \u043f\u043e\u043b\u0435\u0439 \u0432\u0432\u043e\u0434\u0430 \u0438 \u0438\u0445 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439.<\/p>\n<pre><code class=\"typescript\">\/\/ pages\/index.tsx import React, { useState, useMemo } from 'react';  const Home: React.FC = () => {   const [youtubeUrl, setYoutubeUrl] = useState(\"\");   const [startTime, setStartTime] = useState(\"\");   const [endTime, setEndTime] = useState(\"\");    const validYoutubeUrl = useMemo(() => {     const youtubeUrlRegex = \/^(?:https?:\\\/\\\/)?(?:www\\.)?(?:youtu\\.be\\\/|youtube\\.com\\\/(?:embed\\\/|v\\\/|watch\\?v=|watch\\?.+&amp;v=))((\\w|-){11})(?:\\S+)?$\/;     return youtubeUrl.match(youtubeUrlRegex);   }, [youtubeUrl]);    return (     &lt;>       &lt;input        className={`input ${youtubeUrl === \"\" ? \"is-dark\" : validYoutubeUrl? \"is-success\": \"is-danger\" }`}        type=\"text\"        placeholder=\"Youtube URL, eg: https:\/\/www.youtube.com\/watch?v=I-QfPUz1es8\"        value={youtubeUrl}        onChange={(e) => {          setYoutubeUrl(e.target.value);        }}       \/>        &lt;input         className=\"input is-dark\"         type=\"number\"         placeholder=\"Start Second, eg: 38\"         value={startTime}         onChange={(e) => {          setStartTime(e.target.value);         }}        \/>        &lt;input         className=\"input is-dark\"         type=\"number\"         placeholder=\"End Second, eg: 72\"         value={endTime}         onChange={(e) => {          setEndTime(e.target.value);         }}       \/>     &lt;\/>    ) }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u043c\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0441\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0440\u0435\u0433\u0443\u043b\u044f\u0440\u043d\u044b\u0445 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u0439. \u042d\u0442\u043e \u043d\u0435 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f \u0445\u043e\u0440\u043e\u0448\u0435\u0439 \u0432\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u0432\u044f\u0437\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043b\u044f \u0443\u0441\u043b\u043e\u0432\u043d\u043e\u0433\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e YouTube \u043f\u043b\u0435\u0435\u0440\u0430 \u043f\u043e\u0437\u0436\u0435, \u0447\u0442\u043e\u0431\u044b \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043f\u043e\u043a\u0430\u0437\u0430 \u043f\u0443\u0441\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0438\u0448\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 YouTube \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044c.<\/p>\n<p>\u041c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044c \u0438\u0437 <a href=\"https:\/\/github.com\/tjallingt\/react-youtube\"><u>react-youtube<\/u><\/a><\/p>\n<pre><code class=\"typescript\">\/\/ pages\/index.tsx import React, { useState, useMemo } from 'react'; import YouTube from \"react-youtube\";  const Home: React.FC = () => {   \/\/ ...code from before   const [ytPlayer, setYtPlayer] = useState(null);   const ytVideoId = useMemo(() => {     return youtubeUrl.split(\"v=\")[1]?.slice(0, 11);    }, [youtubeUrl]);    return (     &lt;>       &lt;div className=\"content\">          {validYoutubeUrl ? (            &lt;>              &lt;h3>Preview&lt;\/h3>              &lt;YouTube                videoId={ytVideoId}                opts={{                  playerVars: {                  start: Number(startTime),                  end: Number(endTime),                  autoplay: 0,                 },                }}                onReady={(e) => {                 setYtPlayer(e.target);                }}              \/>             &lt;\/>          ) : (            &lt;h4>No Youtube Video Link Selected&lt;\/h4>         )}       &lt;\/div>     &lt;\/>    ) }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u043c\u044b \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043b\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 <code>ytPlayer<\/code> \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c event target. \u041c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0437\u0436\u0435, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043f\u043b\u0435\u0435\u0440\u043e\u043c \u2014 \u0432 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438, \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430.<\/p>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0438\u0448\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u0432\u0435 \u043a\u043d\u043e\u043f\u043a\u0438: Preview \u0438 Generate.<\/p>\n<ul>\n<li>\n<p>\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 (Preview): \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u0432\u0438\u0434\u0435\u043e, \u0447\u0442\u043e\u0431\u044b \u0434\u0430\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c GIF.<\/p>\n<\/li>\n<li>\n<p>\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c (Generate): \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF, \u0442\u043e \u0435\u0441\u0442\u044c \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<\/ul>\n<pre><code class=\"typescript\">\/\/ pages\/index.tsx import React, { useState } from 'react'; import axios from \"axios\"; import { useRouter } from \"next\/router\";  const Home: React.FC = () => {   \/\/ ... code from before   const router = useRouter();   const [loading, setLoading] = useState(false);   const submitYoutubeVideo = async () => {     setLoading(true);     try {       const response = await axios.post(         `${process.env.NEXT_PUBLIC_BASE_URL}\/api\/v1\/jobs`,         {           youtubeUrl,           startTime: Number(startTime),           endTime: Number(endTime),         },         {}       );       router.push(`\/jobs\/${response.data.id}`);     } catch (err) {       alert(err?.response?.data?.message || \"Something went wrong\");     }     setLoading(false);   };    return (     &lt;>      &lt;button       className=\"button is-black\"       onClick={() => {        if (ytPlayer)          ytPlayer.loadVideoById({            videoId: ytVideoId,            startSeconds: Number(startTime),            endSeconds: Number(endTime),           });        }}       >        Preview       &lt;\/button>        &lt;button        className={`button is-black is-outlined ${loading ? \"is-loading\" : \"\"}`}        onClick={submitYoutubeVideo}        >         Generate GIF        &lt;\/button>     &lt;\/>    ) }<\/code><\/pre>\n<h4>\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0432\u0441\u0451 \u0432\u043c\u0435\u0441\u0442\u0435<\/h4>\n<pre><code class=\"typescript\">\/\/ pages\/index.tsx import axios from \"axios\"; import { useRouter } from \"next\/router\"; import React, { useMemo, useState } from \"react\"; import YouTube from \"react-youtube\";  const Home: React.FC = () => {   const router = useRouter();    const [youtubeUrl, setYoutubeUrl] = useState(\"\");   const [startTime, setStartTime] = useState(\"\");   const [endTime, setEndTime] = useState(\"\");   const [loading, setLoading] = useState(false);   const [ytPlayer, setYtPlayer] = useState(null);    const validYoutubeUrl = useMemo(() => {     const youtubeUrlRegex = \/^(?:https?:\\\/\\\/)?(?:www\\.)?(?:youtu\\.be\\\/|youtube\\.com\\\/(?:embed\\\/|v\\\/|watch\\?v=|watch\\?.+&amp;v=))((\\w|-){11})(?:\\S+)?$\/;     return youtubeUrl.match(youtubeUrlRegex);   }, [youtubeUrl]);    const ytVideoId = useMemo(() => {     return youtubeUrl.split(\"v=\")[1]?.slice(0, 11);   }, [youtubeUrl]);    const submitYoutubeVideo = async () => {     setLoading(true);     try {       const response = await axios.post(         `${process.env.NEXT_PUBLIC_BASE_URL}\/api\/v1\/jobs`,         {           youtubeUrl,           startTime: Number(startTime),           endTime: Number(endTime),         },         {}       );       router.push(`\/jobs\/${response.data.id}`);     } catch (err) {       console.log(err);       alert(err?.response?.data?.message || \"Something went wrong\");     }     setLoading(false);   };   return (     &lt;>       {validYoutubeUrl ? (         &lt;>           &lt;h3>Preview&lt;\/h3>           &lt;YouTube             videoId={ytVideoId}             opts={{               playerVars: {                 start: Number(startTime),                 end: Number(endTime),                 autoplay: 0,               },             }}             onReady={(e) => {               setYtPlayer(e.target);             }}           \/>         &lt;\/>       ) : (         &lt;h4>No Youtube Video Link Selected&lt;\/h4>       )}        &lt;input         className={`input ${youtubeUrl === \"\"? \"is-dark\": validYoutubeUrl? \"is-success\": \"is-danger\"}`}         type=\"text\"         placeholder=\"Youtube URL, eg: https:\/\/www.youtube.com\/watch?v=I-QfPUz1es8\"         value={youtubeUrl}         onChange={(e) => {           setYoutubeUrl(e.target.value);         }}       \/>        &lt;input         className=\"input is-dark\"         type=\"number\"         placeholder=\"Start Second, eg: 38\"         value={startTime}         onChange={(e) => {           setStartTime(e.target.value);         }}       \/>        &lt;input         className=\"input is-dark\"         type=\"number\"         placeholder=\"End Second, eg: 72\"         value={endTime}         onChange={(e) => {           setEndTime(e.target.value);         }}       \/>        &lt;button         className={`button is-black`}         onClick={() => {           if (ytPlayer)             ytPlayer.loadVideoById({               videoId: ytVideoId,               startSeconds: Number(startTime),               endSeconds: Number(endTime),             });         }}       >         Preview       &lt;\/button>        &lt;button         className={`button is-black is-outlined ${loading ? \"is-loading\" : \"\"}`}         onClick={submitYoutubeVideo}       >         Generate GIF       &lt;\/button>     &lt;\/>   ); };  export default Home;<\/code><\/pre>\n<h3>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 GIF<\/h3>\n<h4>\u0427\u0430\u0441\u0442\u044b\u0435 \u043e\u043f\u0440\u043e\u0441\u044b \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f GIF<\/h4>\n<p>\u0417\u0434\u0435\u0441\u044c \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u043f\u0435\u0440\u0438\u043e\u0434\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f GIF \u0438\u0437 \u0431\u044d\u043a\u0435\u043d\u0434\u0430. \u042d\u0442\u043e \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e \u043a\u0430\u043a \u0447\u0430\u0441\u0442\u044b\u0435 \u043e\u043f\u0440\u043e\u0441\u044b (polling \/ \u043f\u043e\u043b\u043b\u0438\u043d\u0433).<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <a href=\"https:\/\/swr.vercel.app\/\"><u>swr<\/u><\/a> \u2014 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0441\u0431\u043e\u0440\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f React. \u041e\u043d\u0430 \u043d\u0435 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043e\u0433\u043e \u043e\u043f\u0440\u043e\u0441\u0430, \u043d\u043e \u0443 \u043d\u0435\u0451 \u0435\u0441\u0442\u044c \u0445\u043e\u0440\u043e\u0448\u0438\u0439 API, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0447\u0430\u0441\u0442\u044b\u0439 \u043e\u043f\u0440\u043e\u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442. \u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442 \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u043f\u043e\u0445\u043e\u0436\u0438\u043c\u0438 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044f\u043c\u0438, \u0432 \u0447\u0430\u0441\u0442\u043d\u043e\u0441\u0442\u0438 <a href=\"https:\/\/react-query.tanstack.com\/\"><u>React Query<\/u><\/a>. \u0422\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043e\u043f\u0440\u043e\u0441 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e axios (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0430\u0439\u043c\u0430\u0443\u0442\u044b). \u041e\u0434\u043d\u0430\u043a\u043e \u0442\u0430\u043a\u0438\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0441\u0431\u043e\u0440\u0430 \u0434\u0430\u043d\u043d\u044b\u0445, \u043a\u0430\u043a swr \u0438 React Query, \u0438\u043c\u0435\u044e\u0442 \u0445\u0443\u043a\u0438 \u0434\u043b\u044f \u0441\u0431\u043e\u0440\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 \u044d\u0442\u043e \u0443\u043b\u0443\u0447\u0448\u0430\u0435\u0442 \u043e\u043f\u044b\u0442 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0434\u0440\u0443\u0433\u0438\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435).<\/p>\n<p>\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u0432\u044b\u0431\u043e\u0440\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445:<\/p>\n<pre><code class=\"typescript\">import axios from \"axios\"; import Job from \"..\/..\/common\/interfaces\/Job.interface\";  export default async function fetchJobById(jobId: string): Promise&lt;Job> {   try {     const response = await axios.get(       `${process.env.NEXT_PUBLIC_BASE_URL}\/api\/v1\/jobs\/${jobId}`     );      return response.data;   } catch (err) {     if (err.response?.status === 404) window.location.href = \"\/404\";     throw err;   } }<\/code><\/pre>\n<p>\u0417\u0430\u0442\u0435\u043c \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 swr \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044e GIF:<\/p>\n<pre><code class=\"typescript\">\/\/ pages\/jobs\/[id].tsx import { useRouter } from \"next\/router\"; import React from \"react\"; import useSWR from \"swr\"; import Job from \"..\/..\/lib\/common\/interfaces\/Job.interface\"; import fetchJobById from \"..\/..\/lib\/requests\/fetchers\/jobById\";  export default function JobPage() {   const router = useRouter()   const { jobId } = router.query   const [jobDone, setJobDone] = React.useState(false);     const { data: job, error: errorJob, isValidating: isValidatingJob } = useSWR(     [`\/api\/jobs\/${jobId}`, jobId],     async (url, jobId) => await fetchJobById(jobId),     {       initialData: null,       revalidateOnFocus: false,   \/\/ job will be polled from the backend every 2 seconds until its status change to 'done'       refreshInterval: jobDone ? 0 : 2000,     }   );    React.useEffect(() => {     if (job?.status === \"done\") setJobDone(true);   }, [job]);    const loadingJob = !job;    return (     &lt;>      {\/* rendering logic *\/}     &lt;\/>   ); }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432 \u044d\u0442\u043e\u043c \u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442\u0435 \u043a\u043e\u0434\u0430 <code>refreshInterval<\/code> \u2014 \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043f\u0440\u043e\u0441\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0431\u044d\u043a\u0435\u043d\u0434\u0430. \u041c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0438 \u0431\u0443\u043b\u0435\u0432\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438, \u0438 \u043a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430, \u043c\u044b \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u043c \u0447\u0430\u0441\u0442\u044b\u0439 \u043e\u043f\u0440\u043e\u0441 \u0431\u044d\u043a\u0435\u043d\u0434\u0430.<\/p>\n<h4>\u0420\u0435\u043d\u0434\u0435\u0440\u0438\u043d\u0433 \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430<\/h4>\n<p>\u041c\u044b \u043c\u043e\u0436\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u043d\u0433 Next \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f ID \u0438\u0437 URL, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043b\u044f \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0439 \u0432\u044b\u0431\u043e\u0440\u043a\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.<\/p>\n<p>\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c <code>getServerSideProps()<\/code><\/p>\n<p>\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 <a href=\"https:\/\/nextjs.org\/docs\/basic-features\/data-fetching#getserversideprops-server-side-rendering\"><u>\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 Next.js<\/u><\/a>.<\/p>\n<pre><code class=\"typescript\">\/\/ pages\/jobs\/[id].tsx \/\/ ...other imports import { InferGetServerSidePropsType } from \"next\";  export const getServerSideProps = async (context) => {   const jobId = context.params.id;   try {     const initialJob: Job = await fetchJobById(jobId);     return { props: { jobId, initialJob: initialJob } };   } catch (err) {     return { props: { jobId, initialJob: null } };   } };   export default function JobPage({   jobId,   initialJob, }: InferGetServerSidePropsType&lt;typeof getServerSideProps>)  {   \/\/...other code   const { data: job, error: errorJob, isValidating: isValidatingJob } = useSWR(     [`\/api\/jobs\/${jobId}`, jobId],     async (url, jobId) => await fetchJobById(jobId),     {       \/\/ use initialJob instead of null       initialData: initialJob,       revalidateOnFocus: false,       refreshInterval: jobDone ? 0 : 2000,     }   );    return (     &lt;>      {\/* rendering logic *\/}     &lt;\/>   ); }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b\u0438 <code>initialJob<\/code> \u0432 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0435 <code>initialData<\/code> \u0432 swr \u043e\u043f\u0446\u0438\u044f\u0445.<\/p>\n<h4>\u0421\u043e\u0431\u0438\u0440\u0430\u0435\u043c \u0432\u0441\u0451 \u0432\u043c\u0435\u0441\u0442\u0435<\/h4>\n<pre><code class=\"typescript\">\/\/ pages\/jobs\/[id].tsx import { InferGetServerSidePropsType } from \"next\"; import React from \"react\"; import useSWR from \"swr\"; import Job from \"..\/..\/lib\/common\/interfaces\/Job.interface\"; import fetchJobById from \"..\/..\/lib\/requests\/fetchers\/jobById\";  export default function JobPage({   jobId,   initialJob, }: InferGetServerSidePropsType&lt;typeof getServerSideProps>) {   const [jobDone, setJobDone] = React.useState(false);    const { data: job, error: errorJob, isValidating: isValidatingJob } = useSWR(     [`\/api\/jobs\/${jobId}`, jobId],     async (url, jobId) => await fetchJobById(jobId),     {       initialData: initialJob,       revalidateOnFocus: false,       refreshInterval: jobDone ? 0 : 2000,     }   );    React.useEffect(() => {     if (job?.status === \"done\") setJobDone(true);   }, [job]);    const loadingJob = !job;    return (     &lt;>       {loadingJob ? (         &lt;>           &lt;h4>Getting conversion status..&lt;\/h4>           &lt;progress className=\"progress is-medium is-dark\" max=\"100\">             45%           &lt;\/progress>         &lt;\/>       ) : (         &lt;div className=\"content\">           {job.status === \"error\" ? (             &lt;h4 style={{ color: \"#FF0000\" }}>Conversion Failed&lt;\/h4>           ) : job.status === \"done\" ? (             &lt;>               {!job.gifUrl ? (                 &lt;h4 style={{ color: \"#FF0000\" }}>Conversion Failed&lt;\/h4>               ) : (                 &lt;>                   &lt;h4>Gif&lt;\/h4>                   &lt;img src={job.gifUrl}>&lt;\/img>                   &lt;h6>                     GIF Url : &lt;a href={job.gifUrl}>{job.gifUrl}&lt;\/a>                   &lt;\/h6>                   &lt;h6>                     Converted from :                     &lt;a href={job.youtubeUrl}>{job.youtubeUrl}&lt;\/a>                   &lt;\/h6>                 &lt;\/>               )}             &lt;\/>           ) : (             &lt;>               &lt;h4>Working..&lt;\/h4>               &lt;h5>Conversion Status : {job.status}&lt;\/h5>               &lt;progress className=\"progress is-medium is-dark\" max=\"100\">                 45%               &lt;\/progress>             &lt;\/>           )}         &lt;\/div>       )}     &lt;\/>   ); }  export const getServerSideProps = async (context) => {   const jobId = context.params.id;   try {     const initialJob: Job = await fetchJobById(jobId);     return { props: { jobId, initialJob: initialJob } };   } catch (err) {     return { props: { jobId, initialJob: null } };   } };<\/code><\/pre>\n<p>\u041d\u0430 \u044d\u0442\u043e\u043c \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u043c \u0438 \u043d\u0430\u0434\u0435\u0435\u043c\u0441\u044f, \u0447\u0442\u043e \u0432\u044b \u0443\u0437\u043d\u0430\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u043d\u043e\u0432\u043e\u0435. \u041f\u043e\u043b\u043d\u044b\u0439 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 <a href=\"https:\/\/github.com\/ragrag\/youtube-gif\"><u>github \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438<\/u><\/a>.<\/p>\n<hr\/>\n<p>\u0412 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u0433\u043b\u0430\u0448\u0430\u0435\u043c \u0432\u0441\u0435\u0445 \u0436\u0435\u043b\u0430\u044e\u0449\u0438\u0445 \u043d\u0430 \u043e\u0442\u043a\u0440\u044b\u0442\u044b\u0439 \u0443\u0440\u043e\u043a &#171;React Testing Library&#187;, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u043e\u0439\u0434\u0451\u0442 \u0441\u0435\u0433\u043e\u0434\u043d\u044f \u0432\u0435\u0447\u0435\u0440\u043e\u043c. \u041d\u0430 \u043d\u0451\u043c \u043d\u0430\u0443\u0447\u0438\u043c\u0441\u044f:<\/p>\n<ol>\n<li>\n<p>\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0442\u044c \u043d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u043f\u043e\u0434\u0445\u043e\u0434 \u043a \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 React.js \u0447\u0435\u0440\u0435\u0437 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435. \u041f\u0438\u0441\u0430\u0442\u044c \u043c\u043e\u0434\u0443\u043b\u044c\u043d\u044b\u0435 \u0442\u0435\u0441\u0442\u044b \u0434\u043b\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432, \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435.<\/p>\n<\/li>\n<li>\n<p>\u0423\u043f\u0440\u043e\u0449\u0430\u0442\u044c \u0440\u0435\u0444\u0430\u043a\u0442\u043e\u0440\u0438\u043d\u0433, \u0441\u043e\u043a\u0440\u0430\u0449\u0430\u0442\u044c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0448\u0438\u0431\u043e\u043a \u043d\u0430 \u043f\u0440\u043e\u0434\u0435 \u0438 \u0443\u043f\u0440\u043e\u0449\u0430\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u0443\u044e \u0440\u0430\u0431\u043e\u0442\u0443.<\/p>\n<\/li>\n<li>\n<p>\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c react testing library \u0438 jest. \u041e\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0442\u0435\u0441\u0442\u043e\u0432\u044b\u0435 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438.<\/p>\n<\/li>\n<\/ol>\n<p>\u0417\u0430\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u0443\u0440\u043e\u043a \u043c\u043e\u0436\u043d\u043e <a href=\"https:\/\/otus.pw\/1n84\/\">\u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043e\u043d\u043b\u0430\u0439\u043d-\u043a\u0443\u0440\u0441\u0430 &#171;React.js Developer&#187;.<\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/793624\/\"> https:\/\/habr.com\/ru\/articles\/793624\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u043e\u043b\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u043d\u0430 <a href=\"https:\/\/github.com\/ragrag\/youtube-gif\"><u>github<\/u><\/a>, \u0430 <a href=\"http:\/\/ytgif.vercel.app\/\"><u>\u043f\u043e \u044d\u0442\u043e\u0439 \u0441\u0441\u044b\u043b\u043a\u0435<\/u><\/a> \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0434\u0435\u043c\u043e-\u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\u00a0<\/p>\n<h2>\u041a\u0440\u0430\u0442\u043a\u0438\u0439 \u043e\u0431\u0437\u043e\u0440<\/h2>\n<h4>\u0418\u0434\u0435\u044f<\/h4>\n<p>\u0418\u0434\u0435\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u2014 \u0434\u0430\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c GIF-\u0444\u0430\u0439\u043b\u044b \u0438\u0437 \u0432\u0438\u0434\u0435\u043e\u0440\u043e\u043b\u0438\u043a\u043e\u0432 \u0441 YouTube. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u043d\u0443\u0436\u043d\u043e\u0435 \u0432\u0438\u0434\u0435\u043e, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0437\u0430\u0434\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0438 \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f GIF.<\/p>\n<h4>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 GIF \u0438\u0437 \u0432\u0438\u0434\u0435\u043e \u043d\u0430 YouTube \u0441 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u043e\u043c.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u043f\u0435\u0440\u0435\u0434 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435\u043c \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<\/ul>\n<h3>\u041f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u044b<\/h3>\n<h4>\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0441\u0438\u0441\u0442\u0435\u043c\u044b<\/h4>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0440\u0430\u0431\u043e\u0442\u0443 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u043c\u0438.<\/p>\n<ul>\n<li>\n<p>\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043d\u0430 React (Next.js). \u041e\u0431\u0449\u0430\u0435\u0442\u0441\u044f \u0441 \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u0443\u0436\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044f\u0445.<\/p>\n<\/li>\n<li>\n<p>\u0411\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430 Node. \u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u043e\u0432\u044b\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e \u0432 \u0431\u0440\u043e\u043a\u0435\u0440 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439.<\/p>\n<\/li>\n<li>\n<p>Node \u043a\u0430\u043a Service Worker. \u0411\u0443\u0434\u0435\u0442 \u043e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u0437\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \/ \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0437\u0430\u0434\u0430\u0447 \u043f\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u2014 RabbitMQ. \u0411\u0443\u0434\u0435\u0442 \u0432\u044b\u0441\u0442\u0443\u043f\u0430\u0442\u044c \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447 (Task Queue), \u0432 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0438, \u0430 Service Worker \u2014 \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u044f\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u043d\u0435\u0451.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u2014 MongoDB. \u0411\u0443\u0434\u0435\u0442 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0437\u0430\u0434\u0430\u0447\u0430\u0445 \u043f\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 GIF.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043c\u0435\u0434\u0438\u0430\u0444\u0430\u0439\u043b\u043e\u0432 \u2014 Google Cloud Storage. \u0411\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 GIF-\u0444\u0430\u0439\u043b\u043e\u0432.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430<\/h4>\n<figure class=\"full-width\">\n<div><figcaption>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u044b<\/figcaption><\/div>\n<\/figure>\n<h4>\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438<\/h4>\n<p>\u042d\u0442\u043e \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043f\u043e\u0442\u043e\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0449\u0438\u0439 \u0432\u0441\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b, \u0443\u043f\u043e\u043c\u044f\u043d\u0443\u0442\u044b\u0435 \u0432\u044b\u0448\u0435.<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438<\/figcaption><\/div>\n<\/figure>\n<p>\u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440.<\/p>\n<hr\/>\n<h2>\u0411\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440<\/h2>\n<h3>\u0424\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438<\/h3>\n<figure class=\"full-width\">\n<div><figcaption>\u0414\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439<\/figcaption><\/div>\n<\/figure>\n<p>\u041a\u0430\u043a \u0432\u0438\u0434\u043d\u043e \u0438\u0437 \u043f\u0440\u0438\u0432\u0435\u0434\u0451\u043d\u043d\u043e\u0439 \u0432\u044b\u0448\u0435 \u0434\u0438\u0430\u0433\u0440\u0430\u043c\u043c\u044b, \u0431\u044d\u043a\u0435\u043d\u0434-\u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u0442\u0440\u0438 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438:<\/p>\n<ul>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF \u043f\u0443\u0442\u0451\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 RabbitMQ, \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u0445, \u0447\u0442\u043e \u0431\u044b\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0430 \u043d\u043e\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430 \u043d\u0430 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e (\u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0437\u0430\u0434\u0430\u0447 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u044c).<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447 \u043f\u0443\u0442\u0451\u043c \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e \u0435\u0451 ID \u0438\u0437 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0442\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h3>\n<p>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u0442\u0440\u0451\u0445 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ul>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440<\/p>\n<\/li>\n<li>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0438\u0437 \u043d\u0438\u0445 \u043e\u0431\u043b\u0430\u0434\u0430\u0435\u0442 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u043e\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0441\u0435\u0439\u0447\u0430\u0441 \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435; \u0430 \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u043c\u0441\u044f, \u043f\u043e\u0447\u0435\u043c\u0443 \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c.<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0431\u044d\u043a\u0435\u043d\u0434\u0430<\/figcaption><\/div>\n<\/figure>\n<h4>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432<\/h4>\n<ul>\n<li>\n<p>\u041e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430\u043c \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432. \u041e\u0431\u044b\u0447\u043d\u043e \u044d\u0442\u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 \u043c\u0430\u0441\u0441\u0438\u0432\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u044b \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u043c &#171;Middleware Chain&#187;, \u0430 \u043a\u043e\u043d\u0435\u0447\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u043c \u0432 \u044d\u0442\u043e\u0439 \u0446\u0435\u043f\u043e\u0447\u043a\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430 (Route Controller).<\/p>\n<\/li>\n<li>\n<p>Middleware Chain \u043e\u0431\u044b\u0447\u043d\u043e \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u00ab\u043f\u0440\u043e\u0432\u0435\u0440\u043e\u043a\u00bb \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0432 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u0437\u0430 \u043c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u0412 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0433\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430-\u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u0430.<\/p>\n<\/li>\n<\/ul>\n<h4>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440<\/h4>\n<ul>\n<li>\n<p>\u0418\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0447\u0438\u0449\u0430\u0435\u0442 \u043f\u0440\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0435\u043b\u0435\u0433\u0438\u0440\u0443\u0435\u0442 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u043c\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u0443.<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442\u044b.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0438 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u043e\u043c\u0443 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043e\u043a.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0421\u0435\u0440\u0432\u0438\u0441<\/h4>\n<ul>\n<li>\n<p>\u0421\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0432\u0441\u044e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0443.<\/p>\n<\/li>\n<li>\n<p>\u0418\u043c\u0435\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u0430\u043d\u043d\u044b\u043c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0440\u043e\u0432\u043d\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0434\u0430\u043d\u043d\u044b\u043c (ORM\/ODM).<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c <em>\u0433\u043b\u0443\u043f\u044b\u043c\u0438<\/em>, \u0442\u043e \u0435\u0441\u0442\u044c \u043d\u0435 \u0438\u043c\u0435\u0442\u044c \u043d\u0438\u043a\u0430\u043a\u0438\u0445 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0435\u0439 \u043e \u0431\u0438\u0437\u043d\u0435\u0441-\u043b\u043e\u0433\u0438\u043a\u0435. \u0412\u0441\u0451, \u0447\u0442\u043e \u043e\u043d\u0438 \u0437\u043d\u0430\u044e\u0442, \u044d\u0442\u043e \u00ab\u043a\u0430\u043a\u043e\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u00bb, \u00ab\u043a\u0430\u043a\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0443\u0436\u043d\u044b \u044d\u0442\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0438\u0441\u0443\u00bb, \u00ab\u043a\u0430\u043a \u0434\u043e\u043b\u0436\u0435\u043d \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u043e\u0442\u0432\u0435\u0442\u00bb. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043d\u0430\u043b\u0438\u0447\u0438\u044f \u0442\u043e\u043b\u0441\u0442\u044b\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u0432 (Fat Controllers).<\/p>\n<h3>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f<\/h3>\n<h4>\u0421\u0445\u0435\u043c\u0430 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445<\/h4>\n<p>\u0412 \u044d\u0442\u043e\u043c \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c <a href=\"https:\/\/typeorm.io\/#\/\"><u>TypeORM<\/u><\/a>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u043e\u0439 ORM \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 TypeScript \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445 (\u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c MongoDB).<\/p>\n<p>\u041c\u044b \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043c \u043a\u0430\u0436\u0434\u0443\u044e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u044e GIF \u0432 \u0432\u0438\u0434\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 (Job), \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0448\u0435\u0439 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0435\u0439 (Collection).<\/p>\n<p>Job Collection \u0432 TypeORM \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">import { BaseEntity, Entity, ObjectID, Column, CreateDateColumn, UpdateDateColumn, ObjectIdColumn } from 'typeorm';  @Entity('jobs') export class Job extends BaseEntity {   @ObjectIdColumn()   id: ObjectID;    @Column({     nullable: false,   })   youtubeUrl: string;    @Column({     nullable: false,   })   youtubeId: string;    @Column({     nullable: true,   })   gifUrl: string;    @Column({     nullable: false,   })   startTime: number;    @Column({     nullable: false,   })   endTime: number;    @Column({     type: 'enum',     enum: ['pending', 'processing', 'done', 'error'],   })   status: 'pending' | 'processing' | 'done' | 'error';    @Column()   @CreateDateColumn()   createdAt: Date;    @Column()   @UpdateDateColumn()   updatedAt: Date; }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0432\u0430\u0436\u043d\u043e \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043f\u043e\u043b\u0435 <code>status<\/code> \u2014 \u043f\u043e \u0441\u0443\u0442\u0438, \u043e\u043d\u043e \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435\u043c (enum), \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0449\u0438\u043c \u043d\u0430 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 GIF. \u0412\u0441\u0435 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u043e\u043b\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442 \u0441\u043e\u0431\u043e\u0439 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438.<\/p>\n<h4>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432<\/h4>\n<p>\u0423 \u043d\u0430\u0441 \u0431\u0443\u0434\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0432\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430.<\/p>\n<ul>\n<li>\n<p>\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0430\u0446\u0438\u0438 GIF.<\/p>\n<\/li>\n<li>\n<p>\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u043e \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0437 \u0435\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u043c \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u043e\u0442 \u043a\u0430\u043a \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043d\u0430\u0448 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u043e\u0432:<\/p>\n<pre><code class=\"typescript\">\/\/routes.interface import { Router } from 'express';  interface Route {   path?: string;   router: Router; }  export default Route;<\/code><\/pre>\n<pre><code class=\"typescript\">\/\/jobs.route.ts import { Router } from 'express'; import { CreateJobDto } from '..\/..\/common\/dtos\/createJob.dto'; import Route from '..\/..\/common\/interfaces\/routes.interface'; import JobsController from '..\/..\/controllers\/jobs.controller'; import validationMiddleware from '..\/middlewares\/validation.middleware';  class JobsRoute implements Route {   public path = '\/jobs';   public router = Router();    constructor(private jobsController = new JobsController()) {     this.initializeRoutes();   }    private initializeRoutes() {     this.router.get(`${this.path}\/:id`, this.jobsController.getJobById);     this.router.post(`${this.path}`, validationMiddleware(CreateJobDto, 'body'), this.jobsController.createJob);   } }  export default JobsRoute;<\/code><\/pre>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043c\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c \u0441\u0432\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a-\u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 DTO \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/github.com\/typestack\/class-validator\"><u>class-validator<\/u><\/a> \u0438 <a href=\"https:\/\/github.com\/typestack\/class-transformer\"><u>class-transformer<\/u><\/a><\/p>\n<pre><code class=\"typescript\">\/\/createJob.dto import { Expose } from 'class-transformer'; import { IsNotEmpty, IsNumber, IsString, Matches } from 'class-validator'; import { IsGreaterThan } from '.\/validators\/isGreaterThan'; import { MaximumDifference } from '.\/validators\/maximumDifference';  export class CreateJobDto {   @IsNotEmpty()   @IsString()   @Matches(\/^(?:https?:\\\/\\\/)?(?:www\\.)?(?:youtu\\.be\\\/|youtube\\.com\\\/(?:embed\\\/|v\\\/|watch\\?v=|watch\\?.+&amp;v=))((\\w|-){11})(?:\\S+)?$\/, {     message: 'Invalid youtube url',   })   @Expose()   public youtubeUrl: string;    @IsNotEmpty()   @IsNumber()   @Expose()   public startTime: number;    @IsNotEmpty()   @IsNumber()   @IsGreaterThan('startTime', {     message: 'end time must be greater than start time',   })   @MaximumDifference('startTime', {     message: 'maximum gif duration is 30 seconds',   })   @Expose()   public endTime: number; }<\/code><\/pre>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e <code>IsGreaterThan<\/code> \u0438 <code>MaximumDifference<\/code> \u2014 \u044d\u0442\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u044b \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0434\u043b\u044f <code>class-validator<\/code>. \u041f\u043e \u0441\u0443\u0442\u0438, \u043e\u043d\u0438 \u0432\u044b\u0433\u043b\u044f\u0434\u044f\u0442 \u0442\u0430\u043a (\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0447\u0438\u0442\u0430\u0439\u0442\u0435 \u0432 <a href=\"https:\/\/github.com\/typestack\/class-validator#custom-validation-decorators\"><u>\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438<\/u><\/a>):<\/p>\n<pre><code class=\"typescript\">\/\/isGreaterThan.ts import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';  export function IsGreaterThan(property: string, validationOptions?: ValidationOptions) {   return function (object: Object, propertyName: string) {     registerDecorator({       name: 'isGreaterThan',       target: object.constructor,       propertyName: propertyName,       constraints: [property],       options: validationOptions,       validator: {         validate(value: any, args: ValidationArguments) {           const [relatedPropertyName] = args.constraints;           const relatedValue = (args.object as any)[relatedPropertyName];           return typeof value === 'number' &amp;&amp; typeof relatedValue === 'number' &amp;&amp; value > relatedValue;          },       },     });   }; }<\/code><\/pre>\n<p><code>MaximumDifference<\/code> \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e, \u043d\u043e \u0435\u0433\u043e \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">return typeof value === 'number' &amp;&amp; typeof relatedValue === 'number' &amp;&amp; value - relatedValue &lt;= difference; <\/code><\/pre>\n<p>\u0418 \u0442\u0435\u043f\u0435\u0440\u044c \u043d\u0430\u0448 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a (validation middleware) \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"typescript\">validation.middleware.ts import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; import { RequestHandler } from 'express';  const validationMiddleware = (type: any, value: string | 'body' | 'query' | 'params' = 'body', skipMissingProperties = false): RequestHandler => {   return (req, res, next) => {     validate(plainToClass(type, req[value]), { skipMissingProperties }).then((errors: ValidationError[]) => {       if (errors.length > 0) {         const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', ');         res.status(400).send(message);       } else {         next();       }     });   }; };  export default validationMiddleware; <\/code><\/pre>\n<h4>\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440<\/h4>\n<p>\u041d\u0430\u0448 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e. \u0415\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0435, \u0447\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u2014 \u044d\u0442\u043e \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 <code>CreateJobDto<\/code> \u0438\u0437 \u0442\u0435\u043b\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <code>plainToClass<\/code> \u0438\u0437 <code>class-transformer<\/code> \u0441 <code>excludeExtraneousValues: true<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0443\u043d\u0438\u0447\u0442\u043e\u0436\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044f (\u0441 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u043c <code><a class=\"mention\" href=\"\/users\/Expose\">@Expose<\/a>()<\/code> \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 <code>CreateJobDto<\/code>) \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c <a href=\"https:\/\/github.com\/typestack\/class-transformer#plaintoclass\"><u>\u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u043f\u043e class-transformer.<\/u><\/a><\/p>\n<pre><code class=\"typescript\">\/\/jobs.controllers.ts import { plainToClass } from 'class-transformer'; import { NextFunction, Request, Response } from 'express'; import { CreateJobDto } from '..\/common\/dtos\/createJob.dto'; import { Job } from '..\/entities\/jobs.entity'; import JobsService from '..\/services\/jobs.service';  class JobsController {   constructor(private jobService = new JobsService()) {}    public createJob = async (req: Request, res: Response, next: NextFunction): Promise&lt;void> => {     try {       const jobDto: CreateJobDto = plainToClass(CreateJobDto, req.body, { excludeExtraneousValues: true });       const createdJob: Job = await this.jobService.createJob(jobDto);<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-368166","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/368166","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=368166"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/368166\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=368166"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=368166"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=368166"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}