diff --git a/modules/events-mod/src/main/typescript/control.ts b/modules/events-mod/src/main/typescript/control.ts index df1cacc..296e0ce 100644 --- a/modules/events-mod/src/main/typescript/control.ts +++ b/modules/events-mod/src/main/typescript/control.ts @@ -1,6 +1,7 @@ import {handleConfigurationUpdate} from "./main/config-update/handleConfigurationUpdate"; import {emitPrototypes} from "./main/config-update/prototypes"; import {Queue} from "./main/queue/queue"; +import {EventDataCache} from "./main/cache/EventDataCache"; require("./main/events/registerHandlers") require("./main/commands/kafkatorioCommands") @@ -10,4 +11,5 @@ script.on_configuration_changed(handleConfigurationUpdate) script.on_init(() => { Queue.reset() emitPrototypes() + EventDataCache.init() }) diff --git a/modules/events-mod/src/main/typescript/main/cache/EventDataCache.ts b/modules/events-mod/src/main/typescript/main/cache/EventDataCache.ts new file mode 100644 index 0000000..3252242 --- /dev/null +++ b/modules/events-mod/src/main/typescript/main/cache/EventDataCache.ts @@ -0,0 +1,169 @@ +import {ExcludeNullKeys} from "../global"; + +export namespace EventDataCache { + + + declare const global: { + cache: LuaTable, CacheEntry>, + DEFAULT_EXPIRATION_DURATION_TICKS: uint, + } + + + export function init() { + if (typeof global.cache == undefined) { + global.cache = new LuaTable, CacheEntry>() + + global.DEFAULT_EXPIRATION_DURATION_TICKS = 60 * 60 // ~60 seconds + } + } + + /** + * Update the cache data without restarting the 'time-to-emit' countdown, so that the data won't + * be emitted more than once per {@link CacheEntry.expirationDurationTicks}. + */ + export function throttle( + key: CacheKey, + mutate: (data: CacheData | undefined) => void, + ) { + update(key, mutate, false) + } + + /** + * Update the cache data and reset the 'time-to-emit' countdown, so the data won't be emitted + * until there's an inactivity gap of {@link CacheEntry.expirationDurationTicks}. + */ + export function debounce( + key: CacheKey, + mutate: (data: CacheData | undefined) => void, + ) { + update(key, mutate, true) + } + + function update( + key: CacheKey, + mutate: (data: CacheData | undefined) => void, + resetLastUpdated: boolean, + ) { + const entry = getCacheEntry(key) + mutate(entry?.data) + if (entry != undefined && resetLastUpdated) { + entry.lastUpdatedTick = game.tick + } + } + + /** + * Usage example: If an urgent update comes in, then the expiration can be reduced so that + * it's transmitted sooner. + */ + export function setExpiration( + key: CacheKey, + expirationDurationTicks?: uint, + ) { + const entry = getCacheEntry(key) + if (entry != undefined) { + entry.expirationDurationTicks = expirationDurationTicks + } + } + + function getCacheEntry( + key: CacheKey + ): CacheEntry | undefined { + if (!global.cache.has(key)) { + return undefined + } + const value: CacheEntry = global.cache.get(key) + if (isType(value, key.updateType)) { + return value + } else { + // Type mismatch. This shouldn't happen... + return undefined + } + } + + type UpdateType = T["updateType"] + + /** Only include the non-null properties */ + export type CacheKey = ExcludeNullKeys & { + updateType: UpdateType + } + + /** Make the generated schema objects generic closed-polymorphic */ + export type CacheData = Omit & { + updateType: UpdateType + } + + + class CacheEntry { + lastUpdatedTick: uint = game.tick + expirationDurationTicks?: uint + + data: CacheData + + constructor( + data: CacheData, + expirationDurationTicks?: uint, + ) { + this.data = data + if (expirationDurationTicks != undefined) { + this.expirationDurationTicks = expirationDurationTicks + } + } + + isExpired(): boolean { + return game.tick - + this.lastUpdatedTick > + (this.expirationDurationTicks ?? global.DEFAULT_EXPIRATION_DURATION_TICKS) + } + } + + function isType( + data: CacheEntry, + updateType: UpdateType, + ): data is CacheEntry { + return data.data.updateType == updateType + } + +} + +// interface FactorioEventUpdate { +// type: string +// } +// +// interface Update { +// type: T +// } +// +// export interface PlayerUpdate extends Update<"PLAYER"> { +// index: uint +// +// unhandledEvents?: string[] +// +// characterUnitNumber?: uint +// chatColour?: Colour +// colour?: Colour +// disconnectReason?: string +// forceIndex?: uint +// isAdmin?: boolean +// isConnected?: boolean +// isShowOnMap?: boolean +// isSpectator?: boolean +// lastOnline?: uint +// afkTime?: uint +// name?: string +// onlineTime?: uint +// position?: MapEntityPosition +// surfaceIndex?: uint +// ticksToRespawn?: uint +// tag?: string +// } + +// export type CacheKey = MapChunkCacheKey | EntityCacheKey | PlayerCacheKey & { +// type: Type +// } +// +// export function isCacheType( +// cacheKey: CacheKey, +// type: CacheType +// ): cacheKey is CK { +// return cacheKey.type == type +// } diff --git a/modules/events-mod/src/main/typescript/main/global.d.ts b/modules/events-mod/src/main/typescript/main/global.d.ts index 7a73643..b8122e7 100644 --- a/modules/events-mod/src/main/typescript/main/global.d.ts +++ b/modules/events-mod/src/main/typescript/main/global.d.ts @@ -2,3 +2,11 @@ declare const global: { MOD_VERSION: string FACTORIO_VERSION: string } + +export type EventName = keyof typeof defines.events + +type NonNullKeys = { + [P in keyof T]: null extends Exclude ? never : P +}[keyof T]; + +type ExcludeNullKeys = Pick>