refactor(game): refresh committed read models selectively
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { GameApiContext, WorldStateRow } from '../context.js';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { asRecord, buildGameReadModelDomainRevisionKey, isRecord } from '@sammo-ts/common';
|
||||
|
||||
export type MapCityCompact = [number, number, number, number, number, number];
|
||||
export type MapNationCompact = [number, string, string, number];
|
||||
@@ -94,6 +94,24 @@ const resolveSpyList = (meta: Record<string, unknown>): Record<number, number> =
|
||||
const buildBaseMapCacheKey = (ctx: GameApiContext, scope: 'base' | 'public' = 'base'): string =>
|
||||
`sammo:map:${scope}:${ctx.profile.id}:${ctx.profile.scenario}`;
|
||||
|
||||
const loadWorldMapRevision = async (ctx: GameApiContext): Promise<string> => {
|
||||
const redis = ctx.redis as unknown as {
|
||||
hGet?: (key: string, field: string) => Promise<string | null>;
|
||||
};
|
||||
if (typeof redis.hGet !== 'function') {
|
||||
return '0';
|
||||
}
|
||||
try {
|
||||
return (await redis.hGet(buildGameReadModelDomainRevisionKey(ctx.profile.name), 'world')) ?? '0';
|
||||
} catch {
|
||||
// Cache revision lookup must not make the map unavailable.
|
||||
return '0';
|
||||
}
|
||||
};
|
||||
|
||||
export const buildRevisionedBaseMapCacheKey = async (ctx: GameApiContext): Promise<string> =>
|
||||
`${buildBaseMapCacheKey(ctx)}:r${await loadWorldMapRevision(ctx)}`;
|
||||
|
||||
const loadBaseMap = async (
|
||||
ctx: GameApiContext,
|
||||
options?: {
|
||||
@@ -103,7 +121,7 @@ const loadBaseMap = async (
|
||||
}
|
||||
): Promise<BaseMapResult | null> => {
|
||||
const useCache = options?.useCache ?? true;
|
||||
const cacheKey = options?.cacheKey ?? buildBaseMapCacheKey(ctx);
|
||||
const cacheKey = options?.cacheKey ?? (await buildRevisionedBaseMapCacheKey(ctx));
|
||||
const ttlSeconds = options?.ttlSeconds ?? BASE_MAP_TTL_SECONDS;
|
||||
|
||||
if (useCache) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildGameReadModelDomainRevisionKey } from '@sammo-ts/common';
|
||||
|
||||
import { buildRevisionedBaseMapCacheKey } from '../src/maps/worldMap.js';
|
||||
import type { GameApiContext } from '../src/context.js';
|
||||
|
||||
describe('world map revision cache', () => {
|
||||
it('selects a new shared base-map key after a committed world revision', async () => {
|
||||
const reads: Array<[string, string]> = [];
|
||||
const ctx = {
|
||||
profile: { id: 'hwe', name: 'hwe', scenario: 'scenario_2400' },
|
||||
redis: {
|
||||
hGet: async (key: string, field: string) => {
|
||||
reads.push([key, field]);
|
||||
return '12';
|
||||
},
|
||||
},
|
||||
} as unknown as GameApiContext;
|
||||
|
||||
await expect(buildRevisionedBaseMapCacheKey(ctx)).resolves.toBe(
|
||||
'sammo:map:base:hwe:scenario_2400:r12'
|
||||
);
|
||||
expect(reads).toEqual([[buildGameReadModelDomainRevisionKey('hwe'), 'world']]);
|
||||
});
|
||||
|
||||
it('falls back to revision zero when Redis is temporarily unavailable', async () => {
|
||||
const ctx = {
|
||||
profile: { id: 'hwe', name: 'hwe', scenario: 'scenario_2400' },
|
||||
redis: { hGet: async () => Promise.reject(new Error('redis unavailable')) },
|
||||
} as unknown as GameApiContext;
|
||||
|
||||
await expect(buildRevisionedBaseMapCacheKey(ctx)).resolves.toBe(
|
||||
'sammo:map:base:hwe:scenario_2400:r0'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user