From 72848d2703377cb157667ed44531a765f0c943a7 Mon Sep 17 00:00:00 2001 From: TENSIILE Date: Fri, 27 Mar 2026 17:25:39 +0300 Subject: [PATCH 1/6] fix: fixes interruption issues --- demo/express/src/index.ts | 2 +- .../frameworks/express/express.constants.ts | 4 +++ .../frameworks/express/express.service.ts | 30 ++++++++++++++----- .../get-request-id/get-request-id.util.ts | 4 +-- 4 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 src/packages/frameworks/express/express.constants.ts diff --git a/demo/express/src/index.ts b/demo/express/src/index.ts index 814e094..081e13e 100644 --- a/demo/express/src/index.ts +++ b/demo/express/src/index.ts @@ -6,7 +6,7 @@ import { initRequestInterrupts, getAbortSignal } from '@saborter/server/express' const app = express(); const port = process.env['PORT'] || 3000; -initRequestInterrupts(app, { endpointName: '/api/cancel' }); +initRequestInterrupts(app); app.use(cors()); app.use(express.json()); diff --git a/src/packages/frameworks/express/express.constants.ts b/src/packages/frameworks/express/express.constants.ts new file mode 100644 index 0000000..8d8e0dc --- /dev/null +++ b/src/packages/frameworks/express/express.constants.ts @@ -0,0 +1,4 @@ +export const ENDPOINT_NAME = '/api/@cancel'; +export const REQUEST_ID = 'requestId'; +export const REQUEST_NOT_FOUND_MESSAGE = 'Request not found'; +export const ENDPOINT_WAS_INTERRUPTED_MESSAGE = 'The endpoint was interrupted'; diff --git a/src/packages/frameworks/express/express.service.ts b/src/packages/frameworks/express/express.service.ts index 7a4d0db..a304df8 100644 --- a/src/packages/frameworks/express/express.service.ts +++ b/src/packages/frameworks/express/express.service.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction, Express } from 'express'; import { AbortError } from 'saborter/errors'; import { setSignalInExpressRequest } from './express.utils'; +import * as Constants from './express.constants'; import * as Shared from '../../../shared'; /** @@ -76,7 +77,7 @@ class ExpressRequestInterruptionService { setSignalInExpressRequest(req, controller); this.registerAbortableFunction(requestId, () => { - controller.abort(new AbortError('The endpoint was interrupted', { initiator: 'server' })); + controller.abort(new AbortError(Constants.ENDPOINT_WAS_INTERRUPTED_MESSAGE, { initiator: 'server' })); }); req.on('close', () => { @@ -85,6 +86,7 @@ class ExpressRequestInterruptionService { } }); + // Triggers clearing of request id when request completes or fails, excluding the case of abort. res.on('finish', () => { if (!controller.signal.aborted) { this.abortRegistries.delete(requestId); @@ -93,6 +95,14 @@ class ExpressRequestInterruptionService { next(); }; + + public existsRequest = (requestId: string): boolean => { + return this.abortRegistries.has(requestId); + }; + + public removeRequest = (requestId: string): void => { + this.abortRegistries.delete(requestId); + }; } /** @@ -100,26 +110,26 @@ class ExpressRequestInterruptionService { * * This function: * - Adds a middleware that attaches an `AbortSignal` to every request. - * - Adds a POST endpoint (default `/api/cancel`) that can be called to abort a + * - Adds a POST endpoint (default `/api/@cancel`) that can be called to abort a * specific request by sending its request ID in the request body. * * @param app - Express application instance. * @param options - Configuration options. * @param options.endpointName - The path where the abort endpoint will be mounted. - * Defaults to `/api/cancel`. + * Defaults to `/api/@cancel`. * @returns An `ExpressRequestInterruptionService` instance, which can be used * to manually abort requests if needed. */ export const initRequestInterrupts = ( app: Express, - { endpointName = '/api/cancel' }: { endpointName?: string } = {} + { endpointName = Constants.ENDPOINT_NAME }: { endpointName?: string } = {} ) => { const requestInterruptionService = new ExpressRequestInterruptionService(); app.use(requestInterruptionService.expressMiddleware); - app.post(`${endpointName}`, async (req, res) => { - const requestId = await new Promise((resolve) => { + app.post(endpointName, async (req, res) => { + const requestId = await new Promise((resolve) => { let rawBody = ''; req.on('data', (chunk) => { @@ -130,11 +140,17 @@ export const initRequestInterrupts = ( }); }); + if (!requestInterruptionService.existsRequest(requestId)) { + return res.status(202).send(); + } + if (requestId && requestInterruptionService.abort(requestId)) { res.status(200).json({ aborted: true }); } else { - res.status(404).json({ error: 'Request not found' }); + res.status(404).json({ error: Constants.REQUEST_NOT_FOUND_MESSAGE }); } + + requestInterruptionService.removeRequest(requestId); }); }; diff --git a/src/shared/utils/get-request-id/get-request-id.util.ts b/src/shared/utils/get-request-id/get-request-id.util.ts index f86f9f4..4ca87fc 100644 --- a/src/shared/utils/get-request-id/get-request-id.util.ts +++ b/src/shared/utils/get-request-id/get-request-id.util.ts @@ -1,7 +1,7 @@ import * as Constants from './get-request-id.constants'; export const getRequestId = (req: T): string => { - const requestId = req.headers[Constants.X_REQUEST_ID_HEADER]?.toString().split(','); + const requestId = req.headers[Constants.X_REQUEST_ID_HEADER]?.toString(); - return requestId?.[0] ?? ''; + return requestId ?? ''; }; From 5b3d9319c78d08ffe4fe0a9efe94932f14e65dc6 Mon Sep 17 00:00:00 2001 From: TENSIILE Date: Fri, 27 Mar 2026 22:05:56 +0300 Subject: [PATCH 2/6] docs(readme): corrects documentation (#9) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9537d05..d6f21ea 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ import { initRequestInterrupts, getAbortSignal } from '@saborter/server/express' const app = express(); const port = process.env.PORT || 3000; -initRequestInterrupts(app, { endpointName: '/api/abort' }); +initRequestInterrupts(app); app.use(express.json()); app.get('/', async (req, res) => { From 5ce7c716a9201c57144194ca17a92bd61c65cfe9 Mon Sep 17 00:00:00 2001 From: TENSIILE Date: Fri, 27 Mar 2026 22:07:53 +0300 Subject: [PATCH 3/6] fix: fixes interrupt behavior (#9) --- .../frameworks/express/express.constants.ts | 3 - .../frameworks/express/express.service.ts | 69 +------------------ .../frameworks/express/express.utils.ts | 25 +++++++ src/packages/frameworks/express/index.ts | 1 + 4 files changed, 28 insertions(+), 70 deletions(-) diff --git a/src/packages/frameworks/express/express.constants.ts b/src/packages/frameworks/express/express.constants.ts index 8d8e0dc..568f03d 100644 --- a/src/packages/frameworks/express/express.constants.ts +++ b/src/packages/frameworks/express/express.constants.ts @@ -1,4 +1 @@ -export const ENDPOINT_NAME = '/api/@cancel'; -export const REQUEST_ID = 'requestId'; -export const REQUEST_NOT_FOUND_MESSAGE = 'Request not found'; export const ENDPOINT_WAS_INTERRUPTED_MESSAGE = 'The endpoint was interrupted'; diff --git a/src/packages/frameworks/express/express.service.ts b/src/packages/frameworks/express/express.service.ts index a304df8..26dc9a0 100644 --- a/src/packages/frameworks/express/express.service.ts +++ b/src/packages/frameworks/express/express.service.ts @@ -95,14 +95,6 @@ class ExpressRequestInterruptionService { next(); }; - - public existsRequest = (requestId: string): boolean => { - return this.abortRegistries.has(requestId); - }; - - public removeRequest = (requestId: string): void => { - this.abortRegistries.delete(requestId); - }; } /** @@ -110,71 +102,14 @@ class ExpressRequestInterruptionService { * * This function: * - Adds a middleware that attaches an `AbortSignal` to every request. - * - Adds a POST endpoint (default `/api/@cancel`) that can be called to abort a - * specific request by sending its request ID in the request body. * * @param app - Express application instance. - * @param options - Configuration options. - * @param options.endpointName - The path where the abort endpoint will be mounted. - * Defaults to `/api/@cancel`. + * @returns An `ExpressRequestInterruptionService` instance, which can be used * to manually abort requests if needed. */ -export const initRequestInterrupts = ( - app: Express, - { endpointName = Constants.ENDPOINT_NAME }: { endpointName?: string } = {} -) => { +export const initRequestInterrupts = (app: Express): void => { const requestInterruptionService = new ExpressRequestInterruptionService(); app.use(requestInterruptionService.expressMiddleware); - - app.post(endpointName, async (req, res) => { - const requestId = await new Promise((resolve) => { - let rawBody = ''; - - req.on('data', (chunk) => { - rawBody += chunk; - }); - req.on('end', () => { - resolve(rawBody); - }); - }); - - if (!requestInterruptionService.existsRequest(requestId)) { - return res.status(202).send(); - } - - if (requestId && requestInterruptionService.abort(requestId)) { - res.status(200).json({ aborted: true }); - } else { - res.status(404).json({ error: Constants.REQUEST_NOT_FOUND_MESSAGE }); - } - - requestInterruptionService.removeRequest(requestId); - }); }; - -/** - * Retrieves the `signal` property from an Express Request object. - * - * @param {import('express').Request} req - The Express request object. - * @returns {AbortSignal | undefined} The abort signal attached to the request, - * or `undefined` if the property does not exist. - * - * @example - * // In a route handler - * app.get('/data', (req, res) => { - * const signal = getAbortSignal(req); - * fetch('https://api.example.com/data', { signal }) - * .then(response => response.json()) - * .then(data => res.json(data)) - * .catch(err => { - * if (err.name === 'AbortError') { - * res.status(499).end(); - * } else { - * res.status(500).end(); - * } - * }); - * }); - */ -export const getAbortSignal = (req: Request): AbortSignal | undefined => (req as any).signal; diff --git a/src/packages/frameworks/express/express.utils.ts b/src/packages/frameworks/express/express.utils.ts index 02991d8..977f570 100644 --- a/src/packages/frameworks/express/express.utils.ts +++ b/src/packages/frameworks/express/express.utils.ts @@ -21,3 +21,28 @@ import { Request } from 'express'; export const setSignalInExpressRequest = (req: Request, controller: AbortController): void => { (req as any).signal = controller.signal; }; + +/** + * Retrieves the `signal` property from an Express Request object. + * + * @param {import('express').Request} req - The Express request object. + * @returns {AbortSignal | undefined} The abort signal attached to the request, + * or `undefined` if the property does not exist. + * + * @example + * // In a route handler + * app.get('/data', (req, res) => { + * const signal = getAbortSignal(req); + * fetch('https://api.example.com/data', { signal }) + * .then(response => response.json()) + * .then(data => res.json(data)) + * .catch(err => { + * if (err.name === 'AbortError') { + * res.status(499).end(); + * } else { + * res.status(500).end(); + * } + * }); + * }); + */ +export const getAbortSignal = (req: Request): AbortSignal | undefined => (req as any).signal; diff --git a/src/packages/frameworks/express/index.ts b/src/packages/frameworks/express/index.ts index c4fc744..30f6ee8 100644 --- a/src/packages/frameworks/express/index.ts +++ b/src/packages/frameworks/express/index.ts @@ -1 +1,2 @@ export * from './express.service'; +export { getAbortSignal } from './express.utils'; From 4a46518477949a0a7e60fcc67cf7068265cada43 Mon Sep 17 00:00:00 2001 From: TENSIILE Date: Fri, 27 Mar 2026 22:09:57 +0300 Subject: [PATCH 4/6] increases the patch version of the package (#9) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b37fbf..7634383 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@saborter/server", - "version": "1.0.0", + "version": "1.0.1", "description": "Lightweight, tree‑shakeable utility to cancel server tasks on client abort", "main": "dist/express.cjs.js", "module": "dist/express.es.js", From 03b15114589349d6f3c976b93bf7db424d28301e Mon Sep 17 00:00:00 2001 From: TENSIILE Date: Fri, 27 Mar 2026 22:10:43 +0300 Subject: [PATCH 5/6] installs the demo package --- demo/express/package-lock.json | 4 ++-- demo/express/package.json | 4 ++-- demo/express/saborter-server-1.0.0.tgz | Bin 5345 -> 0 bytes demo/express/saborter-server-1.0.1.tgz | Bin 0 -> 4955 bytes 4 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 demo/express/saborter-server-1.0.0.tgz create mode 100644 demo/express/saborter-server-1.0.1.tgz diff --git a/demo/express/package-lock.json b/demo/express/package-lock.json index 401d2e3..171fd00 100644 --- a/demo/express/package-lock.json +++ b/demo/express/package-lock.json @@ -63,9 +63,9 @@ } }, "node_modules/@saborter/server": { - "version": "1.0.0", + "version": "1.0.1", "resolved": "file:saborter-server-1.0.1.tgz", - "integrity": "sha512-B4WuIeOlR3DEqcO/VsKAj2dm4gGrxeGcSI/32W8qmQKkJd9a9inoXl5Zq1XYf32mDDcGjbjl4EgKDbvw4Aznmw==", + "integrity": "sha512-Wc0GR9zyUYXZFxhOeyQiQfgaoBIRXwdLZRsf+2VE1tscOSdSlDJLTxRY/FnD3LSVpKJbU3vBRrOkBODNZi7yOQ==", "license": "MIT", "peerDependencies": { "express": "^4.17.0 || ^5.0.0", diff --git a/demo/express/package.json b/demo/express/package.json index dc747f5..80da26e 100644 --- a/demo/express/package.json +++ b/demo/express/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "node dist/index.js", "build": "tsc", - "dev": "nodemon src/index.ts" + "dev": "nodemon src/index.ts -watch" }, "keywords": [], "author": "", @@ -14,7 +14,7 @@ "dependencies": { "cors": "^2.8.6", "express": "^5.2.1", - "@saborter/server": "file:./saborter-server-1.0.0.tgz" + "@saborter/server": "file:./saborter-server-1.0.1.tgz" }, "devDependencies": { "@types/express": "^5.0.6", diff --git a/demo/express/saborter-server-1.0.0.tgz b/demo/express/saborter-server-1.0.0.tgz deleted file mode 100644 index 0f5d00077c1446901780134ef956942e74834fc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5345 zcmV<76dvmziwFP!00002|Lt5~a~n63_cN>X9oo#EHPjA=lw`><6t9!`HzU>FwPp}{1ssYSDinf!i&vn_-JXjo{(P%Wf zfnNhXj*0)8Olc!vQZ?vxEU1*8|4MqV8|G@H%+hYw+0d9&Hv-PwN#_jdR8o7>y_ z`^|@NuerV3+}(qF&3hl=Nu(m;UbFcD2H9*jA^ZCWd8&z|AeCUgsvew;Ur}Fq0iCdj zo(mpRp%xCkj=50M9WLf$9(sxom0;0yM5gY|B?%MSs-E&732Aj%JMcpyCHO$E6^#N3 zT_L!*@p&YbNPNYGQ@fE#Ooiil9ud=IP9r62u9`9F(d(E*!B7$Zb=#@6pWG~$OWhQL zcC|C+LTPqY+e?`B;+J}$u1Y(hma}4WDixW>?PH>-7x62{)%qb7H<|*DAo|*Y_YOWEyRTz$o zLv3Y)cQctGU-V+jY!z0_Q%p4x2xB6L4Qc7RCN|q-Dz94 zpGa>;NkD~sF@AZNS(Q`sWE?W(RNZRr(0kE*d3ab&Cp=y;BrS`cr2(BXv|=jTsEbn) zr;~=|RA}QNo(qE-~a5ycVS-$sAdd?T$@t;8c9DZ#^#ii4R$-Kh2+ zMLWXPt{mAekWuJVM>7gE3S!P81y@7@ixd?iiM7Ms{=iqh(g52Tk!>ZscjTXUrsbE%w*Ki$yfg-hfj z@*U=ioJZ6qSA;1*PUnKpnWPSHKe-910@AKl511Y%dTG^7+D~rCpW6w_t&3?C*r9WW z)Rwhn?I3#aK&5k3I~1N&L?zPMX*O%#E6F40#ww?EzqzcnGJ=QAhbaNY)wxw27*~gg zE12*k3aZ$_(j6GLH>y6c+Y0)JHkzSXHU?P*+JI%%ON~RehX<8=ch3iV;&u3seo*-L z9{l%kZ+Cwc|LyEQYTn_$kMXRu35sRiEsbC~zKjMaK(8K1R4G<+rv5w577an3p8_rbkBn} zq^S6yvUFjLzEFn7Fx9Pf(_f2_;qVZunIpw-_1<-u2HMcrg3rJI{`~o%`~CN@)c^~i zGcS58v_naaYi7>Az7vb5L22s-Sh@u=%jE`g73skeh|REUP# znLQXJ42Pg`Hz;`U0Nw+=;eN9QEx5m3miZP|OSRwvTO%ntzsBULp-lj>xqZt;b25r| zVD+-pj#$y!{cTugY)tpKV6B1fd2b=gPTo7!*q2ip9snsI`q}#sJ=_$1ECuzjtYwlc zYc7=F{jCnmP#=}C3iekORAOGn{cDC(jsb9)es>g5WS&jn%3^S37lRupg$ICUQ;HB) zjon5HcK%|+{v81Wlp_YvAp&ILV6sBKUI~ zl9EpHP~(=ixf2!iXH{qa5B6A6 z?xt(B2}QKn10WL0u?p@DSD-C@O{7v;{t+UT68>jVK(9aa>F@jSe{*+d_ZI$ti1c^( z{||Wns}~sBMohy5b*kRVtwOc-w#c8Z6ujpkfUYmUJ0AFF{r>|;|L^txoi+Zy^Jw?Z z|3AjV=4OXm+1hYd6YlzrP49THgo)sDDEPOwn`&v(6zexl_6OfI{{{Wu{?(}IL-+A+ z|KHnvbnEowU4DTP5Et2DGgg6BN)7DeExlpth2jihFB+8fzIL1QkN;$l&4f!2~`g=AA^ zB?PD@BD6(+o+%Z}R--XxYL<*WpU)ekZhzS8opc+iX6t6y9Ow@F_#Lr^>a=D3cv;;4W1SC;2F7!l_SkX)PDkK4uA-S{~ zku0Fy$%Og}rJRsZQd2QuAuS4ydMm~2gGvF92} zYdRbDL?@Ldt^a=}RdG!=%}ON&>(R1)WxTYg=dIF+@=?Cn$O7@T!%M4QTA2!68J911 zGEP_+pnOq02a%v7Qxdh1s}AI)kAE(iqF_kV5$1H*TClkw6VR`2!QcED^g2u#n)hv;(7e%j#DHX>wrcpp6pV6Yt=B`lv?xDB+$ZO(p-*1nhTzl>pa&(|*s}D~MI% zo3}#pxmhT_+x51+?T5J=$$G|YyXiglc1j**T-plq*xOwZMAz2Mgf`5ddK@NGvo(MB zskeukc}JU=2x+yrLTqMR*F5(23u5JV{N1PC_I6?PW|mEhZSPT5uEmmyOle@ox$o^1 zs;%5Uk0xwdCrpf^v7vkHHNA&#p(}a#^q2X&MwgMujS?B8eqeGd?emgvrVz5qNV+b(?^L zN%dIF1V>yk->6$pK%CF2t&%emhO0>> zq%a(T-AhFx#YhOa{b7Hr!}KbZ(X%cL&z_FH?hLxn8^ZbE?5o~!_ZX_3A@qh-7ryR| zo}E1(!PlL^pwl0H17}a6)Bgtkz1Kf>q5GfbgYIw$XM;-b^!%jPJ$9kjKRS7S-0Ob{ zUpyZ{|7-*&z0=;Pdkmv9zz!{|UU!JhPP>DnXPy42^F{BZH~Pk{JnfD8$n)vh06K8q z8H{>I&rdo7IDbAkKO1(T(?5p(S-;nRI_UMk?4EY}BM*9g=$}FNt8RY;!)Kk76YQ$e zc|Lk}HbD8{=rzHxO3Y1va4I2 zjh=M}6(lp#!Pn2aScLs``p`KV_0IY@jH9#uXwW$txiC5#jIyR*_l8{;I)mO274md& zcIsA8oo7#xq}RuG{jTAHs)o{xoDKB*^IvaCmWT|2+RbXw5#cH=hfAf7sc~eQlEIH7Io_^8Azzw)qXq&OHzH3A47e{ z=2MWu&$+Vs)RQwt!$5kBH==mnxV**m(B~l+Z9#(vAsN%K9mVs1+i9j9K;y}04e~)` z4)|3RauUejEOjuy#n+RU({~8>E6$G_M8g@`xZ>jVB;;4J;WKYB53R^Yy?4v`&velk zlVD0ez!=6MA2$MeNkbl|5r~!FV$$EFY%Ny9La_R2Z&V+L3H|3}wBmfJDSwAF`87h_ z^j6mvTM()dKS5X9+8WxPVW@lF+5!S$ey0L2`aHm+-qq!mQxR^F*RD+O>1rZBS zGopYbieqlDPr`6vuUw?PbE)G|K45wWF_`UYMh*5Y-oZ#9U{g~)_#$DU0*jCl^aVr& z@M0ye5IX0*Ts%9oRCsg#BMU=fC9OXnHUjR;#@BS*=$!W&D_k&jOo60i9#Sd8g$C;I zqH7L8a{l&I#QJazCW6c<8moyS5DQCK@C4zpR6K?xil|SeL?vMw+n7iSKO_{>`dl4d zfIi@qE}~n^M;CY(-Z}39s!_Ti$)!zt#;YqqB~OHpx!F+wY#w8(Hdgy2Bx8Nw<-yYg zC*an7*x!Hp8F+*>sCFJuEh+I;q9+W~a&$I)5+m+j@I=5kiGq-NXf2<70{{5y&%eNN zF+Rcy#QNo+iAe|;W{@L*M@`U6DhOV`8TW2svpT%!MN*M4)Qw&`pZw#mKmV=H3S~7- z^&biIU&Bxlp)}DiKh=N0B0S*{%FtNfHBA0ew+>m$<^CHKJHzsei;FRlvyBQK-Pm3s zbwyZRJ~t|#02dpT1rZUZ#lMU4;^HFf_?MrcAipX81im1W`S4tlDMnBNtesi0^5e{n zicOZVc4rbGHtpNyK~{eQOy*xWq?r<%nP?#!m#NofLb$6L87Z69AvOvEY}l+*665y} zw!*0$q~)>6w~ht(^%Yfg>78RB@#YQeHk(b&VXZU{iR0TzteZN4Rg1HUd6XL!63044 zHN8#JNtm^oW{h>1pS{`G%+gZ@{lP^yGTEWHx+<|q4+(7sHI+%IvW(S`N7F$P;UU6V zx_~)lRUud1r%AB3QW!p_cnLmH{tO(Q9a>8}#N_Pa)YYx?+c&66|= zoV3R!4mR!DQte^hYJm!mA9JmQm~-JKH^^dnft)q_qVfOt|G=nV%f|MtZH*Q&@?=qJ{9r8r>3S2rGzC1`#Ki7M zA2;Coge%uYS@jn%u^Hp8Zsd~)0_ul&hM~zV-wTr+J|RABt#P!p3FalpnD{i4OY0?T zn~IrEkh#@9Y(X)NFhB0K>gY^xsX%c|KV(H-CaAB%1x~hL^wLOfzu?e5s85MZ&+DZB z^>!=h4>s2rXKRhA7ANrMzoaL1SX4N;gE~k`;e|y|294`0u0U|D_H_Wcu^_oovrpx5uTfN3z9YeS?ikV{2=p0$VV! z{v^#(Anl3~+eBm2l-yc_>_=xF^3Z%fkBK02xG|zQS!XrRwpgNfLLCqcrOd=_W`*zF ziW}`4NFtL09W23~oeGsM07NO`+f&%WI^b%C*~0}~WLz$AV?o+%qyeW=e~Cb^nJkWs zUXytoQvK3sKrccJ&J#spMiBf7U6b#mh+;S0HT&pRwtr@yo~_v=4=m+`Du3o|rofuS z%(IecXl;ABNqH@6*=RjAqk6%LIj%(QCy@S5_x=@9^A+Wjd|$?bB9-2mapx@#CEcB= z5caFib{jVHoqDrYB86K!bhh=TBuk*7q;GGfckJ7(vfoQvZvQqm(!AGn?=spiE4`&> zs+Bv8p0BqUU&(iR`s1mP{YZ0D&OWe0m@ku9caIBx!G}m3hs@WzO>Sl*Jn>IA>Xt65 zLnjC@;+y9mwwXB6*gJ+ea8cUn6$cC@7Fm#3W^{fw9OWN`IRTvz+z&4bA3a^vOdq)G zj;W5ZjAI#(BUafmraqf6KO6XX0f?nEWKp?3ddKN^uNff^7Pr<71jqGt3NnF5P)-^(#`*D~h`-lKWt)nx5+Y!s?5 zj7myfS=%}hT-d>`!g|IdM3u~^7uncdxSBD4md;5cX;2nMahQ|HM5$&JR@)LiG@>-H z^1sa`)&>|Yz!0OA?>(#01}qP=?15tWVGGPx${O>cJ6&>Sd?!srhr65p-}3#(2S$It z3;%8J-2MMwclhrP|J~ugJN$Qt|L*YL9saw+e|OK_bNAdmAN2WOTEPiN09pV5Ffm~9 diff --git a/demo/express/saborter-server-1.0.1.tgz b/demo/express/saborter-server-1.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f1a25e33f81fbee3360736a823ec5694375118d7 GIT binary patch literal 4955 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 ZZT!D|ZlBxd_PO!r{{hl6ZPfr&002q0wq*bS literal 0 HcmV?d00001 From 952ef576998ed1c46be900bd07ceaffe35822897 Mon Sep 17 00:00:00 2001 From: TENSIILE Date: Fri, 27 Mar 2026 22:18:30 +0300 Subject: [PATCH 6/6] docs(readme): fixes the badge's functionality (#9) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6f21ea..0665985 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - +