RUNTIME · REBUILDABLE GLOBAL CACHE

Build a Global Cache That Can Disappear Safely

Verification statusChinese source article: Reviewed in full — rebuildability, TTL/version boundaries, per-room keys, ID recovery, and offline-test limits are preserved · Technical correction: The cache contract now leads with rebuildability, explicit invalidation, copied values, and current-tick object resolution · JavaScript syntax: Passed by the editorial batch gate

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full — rebuildability, TTL/version boundaries, per-room keys, ID recovery, and offline-test limits are preserved
Technical correction
The cache contract now leads with rebuildability, explicit invalidation, copied values, and current-tick object resolution
JavaScript syntax
Passed by the editorial batch gate
Official documentation
Checked August 18, 2026 — current-tick Game objects, Memory persistence, global-context reuse/reset behavior, Game.getObjectById(), Game.cpu.getUsed(), bucket, limit, and tickLimit
Static code review
Passed — cache identity includes changing inputs, cache absence is not assigned an invented cause, durable state stays outside disposable cache, live game objects are resolved from the active tick, and cold/warm CPU samples are labeled separately
Performance boundary
No claim is made that the example cache reduces real-shard CPU; the revision only defines a reproducible measurement plan
Evidence level
Official-documentation review, Chinese-source review, and static code review only; no real-shard execution is claimed
Console test pending
Not run in this editorial pass
Screeps Console test
Pending — no real-shard Console transcript was collected for this revision
Live multi-tick verification pending
No live global-reset, cold/warm CPU, heap-pressure, cache-invalidation, ID-resolution, or multi-room performance trace was collected for this revision
Last editorial review
August 18, 2026

Use this guide when

Use this page when a value is expensive to rebuild but safe to lose: a sorted ID list, compiled configuration, static room index, or other derived data. Durable decisions still belong in Memory or Segments. A global cache is useful only when every caller remains correct after the cache disappears.

The runtime may keep global data, but it owes you no lifetime

global.runtimeInfo ??= {
  startedAt: Game.time,
  loopCalls: 0
};

global.runtimeInfo.loopCalls += 1;

const runtimeSnapshot = {
  startedAt: global.runtimeInfo.startedAt,
  currentTick: Game.time,
  loopCalls: global.runtimeInfo.loopCalls
};

This can show that the current runtime survived several ticks. It cannot identify why an earlier runtime ended, and it is not durable history. Code deployment, server behavior, or another global reset can remove the object before the next tick.

Define the rebuild and invalidation contract first

QuestionRequired answer
What is the source of truth?Current game state, code, Memory, or an active Segment
How is the value rebuilt?A deterministic builder or a safe slower path
When is it stale?Explicit version change, age limit, or confirmed state change
What happens after reset?The next caller rebuilds or returns a clear unavailable status

“It was present last tick” is not an invalidation policy.

Cache one concrete derived value

function getRoomIndexCache() {
  global.roomIndexCache ??= new Map();
  return global.roomIndexCache;
}

function getVisibleRoomIndex(
  room,
  version,
  maxAge = 100
) {
  if (
    !room
    || !Number.isInteger(version)
    || !Number.isInteger(maxAge)
    || maxAge < 0
  ) {
    return {
      status: 'arguments-invalid',
      sourceIds: [],
      structureIds: []
    };
  }

  const cache = getRoomIndexCache();
  const entry = cache.get(room.name);
  const fresh = Boolean(
    entry
    && entry.version === version
    && Game.time - entry.builtAt <= maxAge
  );

  if (fresh) {
    return {
      status: 'cache-hit',
      sourceIds: [...entry.sourceIds],
      structureIds: [...entry.structureIds]
    };
  }

  const next = {
    version,
    builtAt: Game.time,
    sourceIds: room.find(FIND_SOURCES)
      .map(source => source.id)
      .sort(),
    structureIds: room.find(FIND_STRUCTURES)
      .map(structure => structure.id)
      .sort()
  };

  cache.set(room.name, next);

  return {
    status: entry
      ? 'cache-rebuilt'
      : 'cache-created',
    sourceIds: [...next.sourceIds],
    structureIds: [...next.structureIds]
  };
}

The builder requires current room visibility. The returned arrays are copies, so a caller cannot reorder or delete the cached arrays accidentally. The example version must change when your cache contract changes; the age limit is a fallback, not proof that the room layout stayed unchanged.

Cache IDs, then resolve current objects

function resolveCurrentObjects(ids) {
  if (!Array.isArray(ids)) {
    return [];
  }

  return ids
    .map(id => Game.getObjectById(id))
    .filter(object => object !== null);
}

Do not keep a Creep, Room, Structure, Source, or other live game object for reuse on later ticks. Cache its ID or derived primitive data and recover the current object when needed. A null result can mean the object is gone or currently outside vision; do not erase remote plans until your visibility policy can distinguish those cases.

Invalidate from an explicit state version

function getRoomLayoutVersion(roomName) {
  const value = Memory.roomLayoutVersion?.[roomName];

  return Number.isInteger(value) && value >= 0
    ? value
    : 0;
}

function bumpRoomLayoutVersion(roomName) {
  Memory.roomLayoutVersion ??= {};
  Memory.roomLayoutVersion[roomName] =
    getRoomLayoutVersion(roomName) + 1;

  return Memory.roomLayoutVersion[roomName];
}

Increment the version after your code confirms a layout change that affects the derived value. Do not increment it every tick. A reset removes the cache but not the persistent version, so the next visible-room read can rebuild under the same contract.

Failure boundaries

  • If the room is not visible and no safe stale result exists, return visibility-required instead of fabricating current data.
  • If the builder throws, keep the old entry untouched or return a clear build failure.
  • If keys can grow without bound, add deterministic eviction.
  • If callers need mutable data, return a copy or a type-specific clone.
  • If rebuilding several entries can exceed the current CPU budget, stagger warm-up work.

How to verify across ticks

Record the runtime start tick, cache status, cache version, and builder inputs. Compare at least one cache hit with one rebuild. A separate real global-reset observation is required before claiming reset recovery. This revision does not contain measured CPU savings or a fabricated reset transcript.

Choose another guide when

Use the Memory guide when the value must survive a reset. Use the Segments guide for larger persistent strings. Use the CostMatrix guide when the cached value is specifically a static navigation layer.

Make the cache key describe the inputs that change the answer

A cache can be internally consistent and still return the wrong answer when its key omits part of the input. A room name is enough only for a value that depends on that room alone. Add explicit schema, configuration, policy, or layout revisions when those inputs change the derived result.

function makeRoomCacheKey(input) {
  if (
    typeof input.roomName !== 'string'
    || !Number.isInteger(input.schemaVersion)
    || !Number.isInteger(input.configRevision)
  ) {
    return null;
  }

  return [
    input.roomName,
    'schema=' + input.schemaVersion,
    'config=' + input.configRevision
  ].join('|');
}

The revision numbers are application data, not Screeps constants. Change them when the represented inputs change. For structure layouts, an explicit layout revision or event-driven invalidation can be more accurate than treating an arbitrary TTL as the only source of truth.

A missing cache does not prove why the runtime state disappeared

Code must handle an empty module/global cache after a fresh runtime context, code reload, explicit heap clearing, or another event that recreates JavaScript state. From global.exampleCache === undefined alone, do not invent the cause. The observable fact is a cache miss; the engineering response is to rebuild from current or durable inputs.

function getOrBuild(key, builder) {
  global.exampleCache ??= new Map();

  if (global.exampleCache.has(key)) {
    return {
      status: 'cache-hit',
      value: global.exampleCache.get(key)
    };
  }

  const value = builder();
  global.exampleCache.set(key, value);

  return {
    status: 'cache-built',
    value
  };
}

If correctness depends on data that cannot be reconstructed after a miss, that state should not live only in disposable global cache.

Measure cold rebuilds and warm hits separately

Do not publish a CPU improvement because one cached call looked cheaper. Measure the same boundary repeatedly and label whether each sample rebuilt or hit the cache. Input size, room visibility, bucket state, code path, and cache state can all change the comparison.

function measureCacheCall(label, read) {
  const before = Game.cpu.getUsed();
  const outcome = read();
  const used = Game.cpu.getUsed() - before;

  return {
    tick: Game.time,
    label,
    cacheStatus: outcome.status,
    used,
    bucket: Game.cpu.bucket,
    limit: Game.cpu.limit
  };
}

This records one section delta; it does not prove a long-run CPU benefit. Keep cold rebuilds and warm hits in separate groups, collect multiple comparable server samples, and leave the performance conclusion Pending until real-shard data exists. Use the CPU profiling guide for measurement boundaries.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.