UNPKG

edge-utils

Version:

Platform-agnostic utilities for edge computing and serverless environments

3 lines (2 loc) 7.84 kB
const t=require("crypto");module.exports={LoadBalancer:class{constructor(t={}){this.algorithm=t.algorithm||"round-robin",this.endpoints=new Map,this.weights=new Map,this.currentIndex=0,this.requestCounts=new Map,this.responseTimes=new Map,this.responseTimeCounts=new Map,this.healthCheckInterval=t.healthCheckInterval||3e4,this.healthCheckUrl=t.healthCheckUrl||"/health",this.failureThreshold=t.failureThreshold||3,this.successThreshold=t.successThreshold||2,this.timeout=t.timeout||5e3,this.performanceHistory=new Map,this.historyWindow=t.historyWindow||100,this.adaptiveWeights=new Map,this.lastWeightUpdate=Date.now(),this.weightUpdateInterval=t.weightUpdateInterval||6e4,t.endpoints&&this.setEndpoints(t.endpoints),this.healthCheckTimer=setInterval(()=>this._performHealthChecks(),this.healthCheckInterval)}setEndpoints(t){this.endpoints.clear(),this.weights.clear(),this.requestCounts.clear(),this.responseTimes.clear(),this.responseTimeCounts.clear(),t.forEach(t=>{this.endpoints.set(t.url,{healthy:!0,failures:0,successes:0}),this.weights.set(t.url,t.weight||1),this.requestCounts.set(t.url,0),this.responseTimes.set(t.url,0),this.responseTimeCounts.set(t.url,0)})}getNextEndpoint(){const t=Array.from(this.endpoints.entries()).filter(([,t])=>t.healthy).map(([t])=>t);if(0===t.length)return null;switch(this.algorithm){case"round-robin":default:return this._roundRobin(t);case"weighted-round-robin":return this._weightedRoundRobin(t);case"least-connections":return this._leastConnections(t);case"random":return this._random(t);case"ip-hash":return this._ipHash(t);case"alter":return this._alterAlgorithm(t)}}recordRequestStart(t){this.requestCounts.set(t,(this.requestCounts.get(t)||0)+1)}recordRequestEnd(t,e,s){if(!this.endpoints.has(t))return;const i=this.requestCounts.get(t)||0;this.requestCounts.set(t,Math.max(0,i-1));const r=this.responseTimes.get(t)||0,h=this.responseTimeCounts.get(t)||0,n=h+1,o=(r*h+e)/n;this.responseTimes.set(t,o),this.responseTimeCounts.set(t,n);const a=this.endpoints.get(t);s?a.successes++:a.failures++,this._recordPerformanceHistory(t,e,s),this._updateCounter++,(this._updateCounter>=100||Date.now()-this._lastWeightUpdate>3e4)&&(this._updateAdaptiveWeights(),this._updateCounter=0,this._lastWeightUpdate=Date.now())}getStats(){const t={totalEndpoints:this.endpoints.size,healthyEndpoints:0,totalRequests:0,algorithm:this.algorithm,endpoints:{}};for(const[e,s]of this.endpoints){const i=s.healthy;i&&t.healthyEndpoints++,t.totalRequests+=this.requestCounts.get(e)||0,t.endpoints[e]={healthy:i,weight:this.weights.get(e)||1,activeRequests:this.requestCounts.get(e)||0,averageResponseTime:this.responseTimes.get(e)||0,failures:s.failures,successes:s.successes}}return t}destroy(){this.healthCheckTimer&&clearInterval(this.healthCheckTimer)}_roundRobin(t){return this.currentIndex>=t.length&&(this.currentIndex=0),t[this.currentIndex++]}_weightedRoundRobin(t){let e=0;const s=[];if(t.forEach(t=>{const i=this.weights.get(t)||1;e+=i,s.push({endpoint:t,weight:i})}),0===e)return t[0];this.currentIndex=(this.currentIndex+1)%e;let i=0;for(const{endpoint:t,weight:e}of s)if(i+=e,this.currentIndex<i)return t;return t[0]}_leastConnections(t){let e=1/0,s=t[0];return t.forEach(t=>{const i=this.requestCounts.get(t)||0;i<e&&(e=i,s=t)}),s}_random(t){return t[Math.floor(Math.random()*t.length)]}_ipHash(e,s){const i=t.createHash("md5").update(s||"default").digest("hex");return e[parseInt(i.substring(0,8),16)%e.length]}_recordPerformanceHistory(t,e,s){this.performanceHistory.has(t)||this.performanceHistory.set(t,[]);const i=this.performanceHistory.get(t);i.push({timestamp:Date.now(),responseTime:e,success:s,activeRequests:this.requestCounts.get(t)||0}),i.length>this.historyWindow&&i.shift()}_updateAdaptiveWeights(){const t=Date.now();for(const e of this.endpoints.keys()){const s=(this.performanceHistory.get(e)||[]).filter(e=>t-e.timestamp<3e5);if(0===s.length){this.adaptiveWeights.set(e,1);continue}const i=s.reduce((t,e)=>t+e.responseTime,0)/s.length,r=s.filter(t=>t.success).length/s.length,h=s.reduce((t,e)=>t+e.activeRequests,0)/s.length,n=this.weights.get(e)||1,o=.6*Math.max(0,1-i/2e3)+.25*r+.15*Math.max(0,1-h/20),a=Math.max(.1,Math.min(3,n*o*2));this.adaptiveWeights.set(e,a)}}_alterAlgorithm(t){if(1===t.length)return t[0];const e=Date.now(),s=t.map(t=>{const s=this.endpoints.get(t),i=this.requestCounts.get(t)||0,r=this.responseTimes.get(t)||100,h=this.responseTimeCounts.get(t)||1,n=s.failures/Math.max(h,1),o=this.adaptiveWeights.get(t)||1,a=(this.performanceHistory.get(t)||[]).filter(t=>e-t.timestamp<3e4);let u=0,c=0;if(a.length>=2){const t=a.slice(-10),e=a.slice(-20,-10);if(e.length>0){const s=t.reduce((t,e)=>t+e.responseTime,0)/t.length,i=e.reduce((t,e)=>t+e.responseTime,0)/e.length;u=(i-s)/i;const r=t.reduce((t,e)=>t+e.activeRequests,0)/t.length,h=e.reduce((t,e)=>t+e.activeRequests,0)/e.length;c=(h-r)/Math.max(h,1)}}return{endpoint:t,score:(.25*Math.max(0,1-i/20)+.25*Math.max(0,1-r/3e3)+.2*Math.max(0,1-n)+.15*Math.min(1,Math.max(0,.5+.5*u+.3*c))+.15*(Math.min(2,o)-1))*(1+.05*Math.random()),metrics:{activeRequests:i,avgResponseTime:r,errorRate:n,adaptiveWeight:o,responseTimeTrend:u,loadTrend:c,totalRequests:h}}});return s.sort((t,e)=>e.score-t.score),s[0].endpoint}async _performHealthChecks(){const t=Array.from(this.endpoints.keys()).map(async t=>{try{const e=new AbortController,s=setTimeout(()=>e.abort(),this.timeout),i=await fetch(`${t}${this.healthCheckUrl}`,{signal:e.signal,headers:{"User-Agent":"Edge-Utils-LoadBalancer"}});clearTimeout(s);const r=i.ok;this._updateHealthStatus(t,r)}catch(e){this._updateHealthStatus(t,!1)}});await Promise.allSettled(t)}_updateHealthStatus(t,e){const s=this.endpoints.get(t);e?(s.successes=Math.min(s.successes+1,this.successThreshold+1),s.failures=0,s.successes>=this.successThreshold&&!s.healthy&&(s.healthy=!0)):(s.failures++,s.successes=0,s.failures>=this.failureThreshold&&s.healthy&&(s.healthy=!1))}},CircuitBreaker:class{constructor(t={}){this.failureThreshold=t.failureThreshold||5,this.recoveryTimeout=t.recoveryTimeout||6e4,this.monitoringPeriod=t.monitoringPeriod||1e4,this.state="CLOSED",this.failures=0,this.lastFailureTime=0,this.successCount=0,this.requestCount=0,this.monitoringStart=Date.now()}async execute(t){if("OPEN"===this.state){if(!(Date.now()-this.lastFailureTime>this.recoveryTimeout))throw new Error("Circuit breaker is OPEN");this.state="HALF_OPEN",this.successCount=0}try{this.requestCount++;const e=await t();return this._recordSuccess(),e}catch(t){throw this._recordFailure(),t}}_recordSuccess(){this.failures=0,"HALF_OPEN"===this.state&&(this.successCount++,this.successCount>=2&&(this.state="CLOSED"))}_recordFailure(){this.failures++,this.lastFailureTime=Date.now(),this.failures>=this.failureThreshold&&(this.state="OPEN")}getStats(){const t=Date.now();if(t-this.monitoringStart>=this.monitoringPeriod){this.monitoringStart=t;const e=this.requestCount>0?this.failures/this.requestCount:0;return{state:this.state,failureRate:e,requestCount:this.requestCount,failures:this.failures,successCount:this.successCount}}return{state:this.state,requestCount:this.requestCount,failures:this.failures,successCount:this.successCount}}},StickySessionManager:class{constructor(t={}){this.storage=t.storage||new Map,this.ttl=t.ttl||18e5,this.hashAlgorithm=t.hashAlgorithm||"md5"}getStickyEndpoint(e,s){const i=`sticky:${e}`,r=Date.now(),h=this.storage.get(i);if(h&&r-h.timestamp<this.ttl&&s.includes(h.endpoint))return h.endpoint;const n=t.createHash(this.hashAlgorithm).update(e).digest("hex"),o=s[parseInt(n.substring(0,8),16)%s.length];return this.storage.set(i,{endpoint:o,timestamp:r}),o}cleanup(){const t=Date.now(),e=[];for(const[s,i]of this.storage)s.startsWith("sticky:")&&t-i.timestamp>this.ttl&&e.push(s);e.forEach(t=>this.storage.delete(t))}}}; //# sourceMappingURL=load-balancing.esm.js.map