fnlb
Version:
Easily run your own bot using FNLB, a powerful and scalable system for managing Fortnite bots.
2 lines (1 loc) • 8.1 kB
JavaScript
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 v,writeFile as D}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 a=Date.now()-this.lastLoadedTime;if(a>=this.staleMs)r=!0,this.log?.(`${this.displayName} is stale (${Math.round(a/1000)}s old), checking for updates...`)}if(!r)return e;let o=await v(e).catch(()=>null),i,s=0,l=this.initialDelayMs;this.log?.(o?"Checking for updates...":`Downloading ${this.displayName}...`);while(s<this.maxDownloadRetries)try{i=await this.fetchReleaseInfo();break}catch(a){let n=Math.min(l*2,this.maxBackoffMs);if(s++,this.error?.("Update error:",a),this.warn?.(`Check for updates attempt ${s} failed: ${a.message}. Retrying in ${n>=60000?`${~~(n/60000)}m`:`${~~(n/1000)}s`}...`),s>=this.maxDownloadRetries)break;await new Promise((g)=>setTimeout(g,l)),l=n}if(!i){if(o)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 Error("[AutoUpdater] Failed to check for updates and no local file found.")}if(o){if(f("sha256").update(o).digest("hex")===i.hash)return this.success?.(`${this.displayName} v${i.version} is up to date`),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Finished loading ${this.displayName} v${i.version}`),e;this.log?.(`Downloading update for ${this.displayName} v${i.version}`)}s=0,l=this.initialDelayMs;while(s<this.maxDownloadRetries)try{let a=await fetch(i.url);if(!a.ok)throw Error(`Download failed with status ${a.status}`);let n=Buffer.from(await a.arrayBuffer());if(f("sha256").update(n).digest("hex")!==i.hash)throw Error("Downloaded file hash mismatch...");return await w(this.storageDir,{recursive:!0}).catch(()=>{throw Error(`Failed to create the target directory on ${this.storageDir}`)}),await D(e,n),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Finished loading ${this.displayName} v${i.version}`),e}catch(a){let n=Math.min(l*2,this.maxBackoffMs);s++,this.error?.("Download error:",a),this.warn?.(`Download attempt ${s} failed: ${a.message}. Retrying in ${n>=60000?`${~~(n/60000)}m`:`${~~(n/1000)}s`}...`),await u.wait(l),l=n}if(o)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 Error(`[AutoUpdater] Failed to download and verify update after ${s} attempts`)}finally{this.lock.unlock()}}getFilePath(){return k(this.storageDir,this.targetFileName)}async fetchReleaseInfo(){if(this.releaseProvider)return this.releaseProvider();if(!this.releaseUrl)throw Error("No release provider configured");let t=await fetch(this.releaseUrl);if(!t.ok)throw 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;lastChannel;shouldRestart=!0;runId=0;constructor(t){this.config=t,this.fnlbDir=t?.fnlbPath?p(t?.fnlbPath,".fnlb"):p(process.cwd(),".fnlb"),this.setupUpdater(t?.channel??"stable")}setupUpdater(t){this.updater=new d({storageDir:this.fnlbDir,targetFileName:`${this.packageName}.mjs`,displayName:"FNLB",releaseUrl:`https://dist.fnlb.net/packages/${this.packageName}/release?channel=${encodeURIComponent(t)}`,maxDownloadRetries:this.config?.maxDownloadRetries??1/0,maxBackoffMs:this.config?.maxBackoffMs??60000,staleMs:this.config?.updateIntervalMs??3600000,log:(...e)=>this.log(...e),success:(...e)=>this.success(...e),warn:(...e)=>this.warn(...e),error:(...e)=>this.error(...e)}),this.lastChannel=t}async start(t){await this.stop(),this.shouldRestart=!0,this.runId++;let e=this.runId;if(!t?.apiToken)throw Error("[FNLB ShardingManager] Please provide a FNLB API token.");let r=t.channel??this.config?.channel??"stable";if(r!==this.lastChannel)this.setupUpdater(r),await this.update(!0);else await this.update();let o=t.numberOfShards??1,i=(~~(Math.random()*1e4)).toString(36)+"fnlb"+(~~(Date.now()/1000)).toString(36);for(let s=0;s<o;s++){let l=`${i}-${s.toString().padStart(2,"0")}`,a=await this.startShard(t,l,e);this.activeProcesses.set(l,a)}}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(!t?.apiToken||t.apiToken.length<10)throw Error("[FNLB ShardingManager] Please provide a valid FNLB API token.");this.log("Starting shard with ID:",e);let o=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,...t.extraEnv},stdio:["inherit","pipe","pipe","ipc"]});if(!this.config?.disableSubProcessLogs)o.stdout?.on("data",(i)=>{let s=i.toString("utf8");process.stdout.write(s),this.config?.onSubProcessLogMessage?.({timestamp:Date.now(),content:s,format:0})});if(!this.config?.disableSubProcessErrorLogs)o.stderr?.on("data",(i)=>{let s=i.toString("utf8");process.stderr.write(s),this.config?.onSubProcessLogMessage?.({timestamp:Date.now(),content:s,format:4})});return o.on("close",async(i)=>{if(this.activeProcesses.delete(e),this.shouldRestart&&r===this.runId){if(i===0)this.warn("Shard exited with code:",i);else this.error("Shard exited with code:",i?.toString()??"none");this.log("Trying to restart shard..."),await this.update(!0),await c.wait(1e4);let s=await this.startShard(t,e,r);this.activeProcesses.set(e,s)}else this.log(`Shard ${e} stopped.`)}),o}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};