forked from OpenWF/SpaceNinjaServer
feat: add world state sync configuration and trigger on config load
This commit is contained in:
parent
3186ffe164
commit
fbe7b40f9b
@ -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,125 @@ 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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
type AnyObj = { [key: string]: object | Array<object> | string | number | boolean };
|
||||||
|
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 = staticWorldState_[name] as Array<object>;
|
||||||
|
if (Array.isArray(remoteValue) && Array.isArray(localValue)) {
|
||||||
|
localValue.push(...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;
|
||||||
|
}
|
||||||
|
let nextSync = Number.MAX_SAFE_INTEGER;
|
||||||
|
if (interval && interval > 0) {
|
||||||
|
nextSync = Date.now() + interval * 1000; // Convert seconds to milliseconds
|
||||||
|
}
|
||||||
|
for (const key in fields) {
|
||||||
|
switch (key) {
|
||||||
|
case "VoidTraders":
|
||||||
|
case "Invasions":
|
||||||
|
case "SyndicateMissions":
|
||||||
|
case "ActiveMissions":
|
||||||
|
case "NodeOverrides":
|
||||||
|
case "PrimeVaultTraders":
|
||||||
|
case "DailyDeals": {
|
||||||
|
if (Array.isArray(staticWorldState_[key])) {
|
||||||
|
const items = staticWorldState_[key] as (
|
||||||
|
| { Expiry?: { $date?: { $numberLong?: string } } }
|
||||||
|
| undefined
|
||||||
|
)[];
|
||||||
|
for (const item of items) {
|
||||||
|
const expire = item?.Expiry?.$date?.$numberLong;
|
||||||
|
if (typeof expire !== "string") continue;
|
||||||
|
const expireMs = parseInt(expire, 10);
|
||||||
|
if (isNaN(expireMs)) continue;
|
||||||
|
nextSync = Math.min(nextSync, expireMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`World state sync completed, next sync at ${nextSync}`);
|
||||||
|
if (nextSync < Number.MAX_SAFE_INTEGER) {
|
||||||
|
let nextSyncDelta = nextSync - Date.now();
|
||||||
|
if (nextSyncDelta < 60 * 1000) {
|
||||||
|
// Don't schedule syncs more often than once per minute
|
||||||
|
logger.warn("Next world state sync is scheduled too soon, delaying it by 1 minute");
|
||||||
|
nextSyncDelta = 60 * 1000;
|
||||||
|
} else {
|
||||||
|
nextSyncDelta += 1000;
|
||||||
|
}
|
||||||
|
logger.info(`Next world state sync in ${Math.round(nextSyncDelta / 1000)} seconds`);
|
||||||
|
syncWorldStateTimer = setTimeout(() => {
|
||||||
|
void syncWorldState();
|
||||||
|
}, nextSyncDelta);
|
||||||
|
} 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