UNPKG

fnlb

Version:

Easily run your own bot using FNLB, a powerful and scalable system for managing Fortnite bots.

2 lines (1 loc) 7.86 kB
import{fork as y}from"node:child_process";import{resolve as p}from"node:path";import{createHash as f}from"node:crypto";import{mkdir as w,readFile as D,writeFile as v}from"node:fs/promises";import{resolve as k}from"node:path";class m{lockPromise;constructor(){this.lockPromise=void 0}get isLocked(){return!!this.lockPromise}wait(){return this.lockPromise?.promise||Promise.resolve()}lock(){let t,e=new Promise((r)=>{t=r});this.lockPromise={promise:e,resolve:t}}unlock(){this.lockPromise?.resolve(),this.lockPromise=void 0}}class u{static wait(t){return new Promise((e)=>setTimeout(e,t))}}class d{storageDir;targetFileName;displayName;releaseUrl;releaseProvider;maxDownloadRetries;maxBackoffMs;initialDelayMs;staleMs;log;success;warn;error;isLoaded=!1;lastLoadedTime=0;lock=new m;constructor(t){this.storageDir=t.storageDir,this.targetFileName=t.targetFileName,this.displayName=t.displayName??t.targetFileName,this.releaseUrl=t.releaseUrl,this.releaseProvider=t.releaseProvider,this.maxDownloadRetries=t.maxDownloadRetries??1/0,this.maxBackoffMs=t.maxBackoffMs??60000,this.initialDelayMs=t.initialDelayMs??1000,this.staleMs=t.staleMs??0,this.log=t.log,this.success=t.success,this.warn=t.warn,this.error=t.error}async ensureUpToDate(t){await this.lock.wait(),this.lock.lock();try{let e=this.getFilePath(),r=t||!this.isLoaded;if(this.isLoaded&&!t&&this.staleMs>0){let o=Date.now()-this.lastLoadedTime;if(o>=this.staleMs)r=!0,this.log?.(`${this.displayName} is stale (${Math.round(o/1000)}s old), checking for updates...`)}if(!r)return e;let a=await D(e,"utf-8").catch(()=>null),s,i=0,l=this.initialDelayMs;this.log?.(a?"Checking for updates...":`Downloading ${this.displayName}...`);while(i<this.maxDownloadRetries)try{s=await this.fetchReleaseInfo();break}catch(o){let n=Math.min(l*2,this.maxBackoffMs);if(i++,this.error?.("Update error:",o),this.warn?.(`Check for updates attempt ${i} failed: ${o.message}. Retrying in ${n>=60000?`${~~(n/60000)}m`:`${~~(n/1000)}s`}...`),i>=this.maxDownloadRetries)break;await new Promise((g)=>setTimeout(g,l)),l=n}if(!s){if(a)return this.warn?.("Failed to check for updates. Using existing local version."),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Loaded existing ${this.displayName} version`),e;throw new Error("[AutoUpdater] Failed to check for updates and no local file found.")}if(a){if(f("sha256").update(a).digest("hex")===s.hash)return this.success?.(`${this.displayName} v${s.version} is up to date`),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Finished loading ${this.displayName} v${s.version}`),e;this.log?.(`Downloading update for ${this.displayName} v${s.version}`)}i=0,l=this.initialDelayMs;while(i<this.maxDownloadRetries)try{let o=await fetch(s.url);if(!o.ok)throw new Error(`Download failed with status ${o.status}`);let n=await o.text();if(f("sha256").update(n).digest("hex")!==s.hash)throw new Error("Downloaded file hash mismatch...");return await w(this.storageDir,{recursive:!0}).catch(()=>{throw new Error(`Failed to create the target directory on ${this.storageDir}`)}),await v(e,n),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Finished loading ${this.displayName} v${s.version}`),e}catch(o){let n=Math.min(l*2,this.maxBackoffMs);i++,this.error?.("Download error:",o),this.warn?.(`Download attempt ${i} failed: ${o.message}. Retrying in ${n>=60000?`${~~(n/60000)}m`:`${~~(n/1000)}s`}...`),await u.wait(l),l=n}if(a)return this.warn?.("Max retries reached. Using existing local version."),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Loaded existing ${this.displayName} version`),e;throw new Error(`[AutoUpdater] Failed to download and verify update after ${i} attempts`)}finally{this.lock.unlock()}}getFilePath(){return k(this.storageDir,this.targetFileName)}async fetchReleaseInfo(){if(this.releaseProvider)return this.releaseProvider();if(!this.releaseUrl)throw new Error("No release provider configured");let t=await fetch(this.releaseUrl);if(!t.ok)throw new Error(`Status code: ${t.status}`);return await t.json()}}class c{static wait(t){return new Promise((e)=>setTimeout(e,t))}}class h{config;activeProcesses=new Map;packageName=`${process.versions.bun?"zenith-bun":"zenith"}`;fnlbDir;updater;shouldRestart=!0;runId=0;constructor(t){this.config=t,this.fnlbDir=t?.fnlbPath?p(t?.fnlbPath,".fnlb"):p(process.cwd(),".fnlb"),this.updater=new d({storageDir:this.fnlbDir,targetFileName:`${this.packageName}.mjs`,displayName:"FNLB",releaseUrl:`https://dist.fnlb.net/packages/${this.packageName}/release`,maxDownloadRetries:this.config?.maxDownloadRetries??1/0,maxBackoffMs:this.config?.maxBackoffMs??60000,staleMs:3600000,log:(...e)=>this.log(...e),success:(...e)=>this.success(...e),warn:(...e)=>this.warn(...e),error:(...e)=>this.error(...e)})}async start(t){await this.stop(),this.shouldRestart=!0,this.runId++;let e=this.runId;if(!t?.apiToken)throw new Error("[FNLB ShardingManager] Please provide a FNLB API token.");await this.update();let r=t.numberOfShards??1,a=(~~(Math.random()*1e4)).toString(36)+"fnlb"+(~~(Date.now()/1000)).toString(36);for(let s=0;s<r;s++){let i=`${a}-${s.toString().padStart(2,"0")}`,l=await this.startShard(t,i,e);this.activeProcesses.set(i,l)}}async stop(){if(this.shouldRestart=!1,this.runId++,this.activeProcesses.size===0)return;this.log("Stopping all active shards...");for(let[t,e]of this.activeProcesses)this.log(`Stopping shard with ID: ${t}`),e.kill();this.activeProcesses.clear(),this.log("All shards stopped.")}async startShard(t,e,r){if(await this.update(),!t?.apiToken||t.apiToken.length<10)throw new Error("[FNLB ShardingManager] Please provide a valid FNLB API token.");this.log("Starting shard with ID:",e);let a=y(p(this.fnlbDir,`${this.packageName}.mjs`),[],{env:{...process.env,FORCE_COLOR:"1",SHARD_ID:e,API_TOKEN:t.apiToken,CATEGORIES:t.categories?.join(","),BOTS_PER_SHARD:(t.botsPerShard??1).toString(),HIDE_USERNAMES:t.hideUsernames?"true":"false",HIDE_EMAILS:t.hideEmails?"true":"false",LOG_LEVEL:t.logLevel,CLUSTER_ID:this.config?.clusterName?.trim().replace(/ +(?= )/g,"").toLowerCase().replaceAll(" ","-")??"unknown",CLUSTER_NAME:this.config?.clusterName?.trim(),FNLB_DIR:this.fnlbDir},stdio:["inherit","pipe","pipe","ipc"]});if(!this.config?.disableSubProcessLogs)a.stdout?.on("data",(s)=>{let i=s.toString("utf8");process.stdout.write(i),this.config?.onSubProcessLogMessage?.({timestamp:Date.now(),content:i,format:0})});if(!this.config?.disableSubProcessErrorLogs)a.stderr?.on("data",(s)=>{let i=s.toString("utf8");process.stderr.write(i),this.config?.onSubProcessLogMessage?.({timestamp:Date.now(),content:i,format:4})});return a.on("close",async(s)=>{if(this.activeProcesses.delete(e),this.shouldRestart&&r===this.runId){if(s===0)this.warn("Shard exited with code:",s);else this.error("Shard exited with code:",s?.toString()??"none");this.log("Trying to restart shard..."),await this.update(!0),await c.wait(1e4);let i=await this.startShard(t,e,r);this.activeProcesses.set(e,i)}else this.log(`Shard ${e} stopped.`)}),a}async update(t){await this.updater.ensureUpToDate(t)}log(...t){if(!this.config?.disableLogs)console.log("[FNLB ShardingManager]",...t),this.config?.onLogMessage?.({timestamp:Date.now(),content:t.join(" "),format:0})}success(...t){if(!this.config?.disableLogs)console.log("[FNLB ShardingManager] [OK]",...t),this.config?.onLogMessage?.({timestamp:Date.now(),content:t.join(" "),format:1})}warn(...t){if(!this.config?.disableErrorLogs)console.warn("[FNLB ShardingManager] [WRN]",...t),this.config?.onLogMessage?.({timestamp:Date.now(),content:t.join(" "),format:3})}error(...t){if(!this.config?.disableErrorLogs)console.error("[FNLB ShardingManager] [ERR]",...t),this.config?.onLogMessage?.({timestamp:Date.now(),content:t.join(" "),format:4})}}var N;((r)=>{r.Info="INFO";r.Debug="DEBUG"})(N||={});var O=h;export{O as default,N as LogLevel};