Allow to sync worldstate from a remote server #2218
@ -21,12 +21,13 @@ import mongoose from "mongoose";
 | 
				
			|||||||
import { JSONStringify } from "json-with-bigint";
 | 
					import { JSONStringify } from "json-with-bigint";
 | 
				
			||||||
import { startWebServer } from "./services/webService";
 | 
					import { startWebServer } from "./services/webService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { validateConfig } from "@/src/services/configWatcherService";
 | 
					import { validateConfig, triggerConfigLoadedCallbacks } from "@/src/services/configWatcherService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Patch JSON.stringify to work flawlessly with Bigints.
 | 
					// Patch JSON.stringify to work flawlessly with Bigints.
 | 
				
			||||||
JSON.stringify = JSONStringify;
 | 
					JSON.stringify = JSONStringify;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
validateConfig();
 | 
					validateConfig();
 | 
				
			||||||
 | 
					triggerConfigLoadedCallbacks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mongoose
 | 
					mongoose
 | 
				
			||||||
    .connect(config.mongodbUrl)
 | 
					    .connect(config.mongodbUrl)
 | 
				
			||||||
 | 
				
			|||||||
@ -71,6 +71,12 @@ export interface IConfig {
 | 
				
			|||||||
        duviriOverride?: string;
 | 
					        duviriOverride?: string;
 | 
				
			||||||
        nightwaveOverride?: string;
 | 
					        nightwaveOverride?: string;
 | 
				
			||||||
        circuitGameModes?: string[];
 | 
					        circuitGameModes?: string[];
 | 
				
			||||||
 | 
					        sync?: {
 | 
				
			||||||
 | 
					            enabled?: boolean;
 | 
				
			||||||
 | 
					            url?: string;
 | 
				
			||||||
 | 
					            fields?: { [key: string]: "replace" | "merge" };
 | 
				
			||||||
 | 
					            interval?: number; // in seconds
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    dev?: {
 | 
					    dev?: {
 | 
				
			||||||
        keepVendorsExpired?: boolean;
 | 
					        keepVendorsExpired?: boolean;
 | 
				
			||||||
 | 
				
			|||||||
@ -17,6 +17,7 @@ fs.watchFile(configPath, () => {
 | 
				
			|||||||
            process.exit(1);
 | 
					            process.exit(1);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        validateConfig();
 | 
					        validateConfig();
 | 
				
			||||||
 | 
					        triggerConfigLoadedCallbacks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const webPorts = getWebPorts();
 | 
					        const webPorts = getWebPorts();
 | 
				
			||||||
        if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
 | 
					        if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
 | 
				
			||||||
@ -50,3 +51,21 @@ export const saveConfig = async (): Promise<void> => {
 | 
				
			|||||||
    amnesia = true;
 | 
					    amnesia = true;
 | 
				
			||||||
    await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2));
 | 
					    await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onConfigLoadedCallbacks = [] as (() => void)[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const onConfigLoaded = (callback: () => void): void => {
 | 
				
			||||||
 | 
					    onConfigLoadedCallbacks.push(callback);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const triggerConfigLoadedCallbacks = (): void => {
 | 
				
			||||||
 | 
					    onConfigLoadedCallbacks.forEach(callback => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            callback();
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            if (e instanceof Error) {
 | 
				
			||||||
 | 
					                logger.error(`Error in config loaded callback: ${e.message}`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMiss
 | 
				
			|||||||
import { buildConfig } from "@/src/services/buildConfigService";
 | 
					import { buildConfig } from "@/src/services/buildConfigService";
 | 
				
			||||||
import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
					import { unixTimesInMs } from "@/src/constants/timeConstants";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { config } from "@/src/services/configService";
 | 
				
			||||||
 | 
					import { onConfigLoaded } from "@/src/services/configWatcherService";
 | 
				
			||||||
import { SRng } from "@/src/services/rngService";
 | 
					import { SRng } from "@/src/services/rngService";
 | 
				
			||||||
import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
 | 
					import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -1491,3 +1492,88 @@ const nightwaveTagToSeason: Record<string, number> = {
 | 
				
			|||||||
    RadioLegionIntermissionSyndicate: 1, // Intermission I
 | 
					    RadioLegionIntermissionSyndicate: 1, // Intermission I
 | 
				
			||||||
    RadioLegionSyndicate: 0 // The Wolf of Saturn Six
 | 
					    RadioLegionSyndicate: 0 // The Wolf of Saturn Six
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AnyObj = { [key: string]: object | Array<object> | string | number | boolean };
 | 
				
			||||||
 | 
					const staticWorldStateBackup: AnyObj = structuredClone(staticWorldState);
 | 
				
			||||||
 | 
					let syncWorldStateTimer: NodeJS.Timeout | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const syncWorldState = async (): Promise<void> => {
 | 
				
			||||||
 | 
					    if (syncWorldStateTimer) {
 | 
				
			||||||
 | 
					        clearTimeout(syncWorldStateTimer);
 | 
				
			||||||
 | 
					        syncWorldStateTimer = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!config.worldState?.sync) {
 | 
				
			||||||
 | 
					        logger.info("World state sync is disabled, skipping");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const { enabled, url, fields, interval } = config.worldState.sync;
 | 
				
			||||||
 | 
					    if (!enabled || !url || !fields) {
 | 
				
			||||||
 | 
					        logger.info("World state sync is not enabled or misconfigured, skipping");
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const res = await fetch(url, { method: "GET" });
 | 
				
			||||||
 | 
					    if (!res.ok) {
 | 
				
			||||||
 | 
					        logger.error("Failed to fetch remote world state, will retry in 5 min", {
 | 
				
			||||||
 | 
					            status: res.status,
 | 
				
			||||||
 | 
					            statusText: res.statusText
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        syncWorldStateTimer = setTimeout(
 | 
				
			||||||
 | 
					            () => {
 | 
				
			||||||
 | 
					                void syncWorldState();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            5 * 60 * 1000
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const data = await res.json();
 | 
				
			||||||
 | 
					    if (!data || typeof data !== "object") {
 | 
				
			||||||
 | 
					        logger.error("Invalid world state sync response", { data });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const staticWorldState_ = staticWorldState as AnyObj;
 | 
				
			||||||
 | 
					    const data_ = data as AnyObj;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const [name, action_] of Object.entries(fields)) {
 | 
				
			||||||
 | 
					        if (!(name in data)) {
 | 
				
			||||||
 | 
					            logger.warn(`Field ${name} not found in world state sync response`, { data });
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const action = action_ as string;
 | 
				
			||||||
 | 
					        if (action === "replace") {
 | 
				
			||||||
 | 
					            staticWorldState_[name] = data_[name];
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (action === "merge") {
 | 
				
			||||||
 | 
					            switch (name) {
 | 
				
			||||||
 | 
					                case "Events":
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        const remoteValue = data_[name] as Array<object>;
 | 
				
			||||||
 | 
					                        const localValue = staticWorldStateBackup[name] as Array<object>;
 | 
				
			||||||
 | 
					                        if (Array.isArray(remoteValue) && Array.isArray(localValue)) {
 | 
				
			||||||
 | 
					                            staticWorldState_[name] = localValue.concat(remoteValue);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    logger.warn(`Not supported merge action for field ${name} in world state sync`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        logger.warn(`Unknown action ${action} for field ${name} in world state sync`);
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (interval && interval > 0) {
 | 
				
			||||||
 | 
					        logger.info(`Next world state sync in ${interval} seconds`);
 | 
				
			||||||
 | 
					        syncWorldStateTimer = setTimeout(() => {
 | 
				
			||||||
 | 
					            void syncWorldState();
 | 
				
			||||||
 | 
					        }, interval * 1000);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.info("No next world state sync scheduled");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onConfigLoaded(() => {
 | 
				
			||||||
 | 
					    if (config.worldState?.sync?.enabled) {
 | 
				
			||||||
 | 
					        void syncWorldState();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user