perf(api): load the discovery set once per location request
getCurrentLocation was calling isTravelAllowed per connection, re-querying the character's discovered locations once per gated exit. Extract the gating rule into a pure, synchronous WorldDiscoveryService.isRouteOpen so the map filter can load the discovery set once and filter in memory, while isTravelAllowed (TravelService's entry point) keeps its exact signature and behaviour by delegating to the same rule. Also pins that discoversLocationKey never reaches the client payload.
This commit is contained in:
@@ -80,7 +80,24 @@ export class WorldDiscoveryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const known = await this.getDiscoveredLocationIds(characterId, manager);
|
const known = await this.getDiscoveredLocationIds(characterId, manager);
|
||||||
return known.has(connection.toLocationId);
|
return this.isRouteOpen(known, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gating rule itself, given an already-loaded discovery set.
|
||||||
|
*
|
||||||
|
* Pure and synchronous so a caller with many connections can load the set
|
||||||
|
* once and filter in memory, while `isTravelAllowed` stays the convenient
|
||||||
|
* single-connection entry point. One rule, two callers.
|
||||||
|
*/
|
||||||
|
isRouteOpen(
|
||||||
|
discoveredLocationIds: ReadonlySet<string>,
|
||||||
|
connection: Pick<LocationConnection, 'toLocationId' | 'requiresDiscovery'>,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
!connection.requiresDiscovery ||
|
||||||
|
discoveredLocationIds.has(connection.toLocationId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private discoveries(manager?: EntityManager) {
|
private discoveries(manager?: EntityManager) {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const BURNED_ROAD_POIS: LocationPointOfInterestContent[] = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
resultTitle: 'Suspicious Tracks',
|
resultTitle: 'Suspicious Tracks',
|
||||||
resultText: 'Fresh bootprints lead east.',
|
resultText: 'Fresh bootprints lead east.',
|
||||||
|
discoversLocationKey: 'ash-pit',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sealed-crypt',
|
key: 'sealed-crypt',
|
||||||
@@ -238,14 +239,15 @@ function buildService(
|
|||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
const worldDiscovery = {
|
const worldDiscovery = {
|
||||||
discover: jest.fn(),
|
discover: jest.fn(),
|
||||||
isTravelAllowed: (
|
getDiscoveredLocationIds: jest
|
||||||
_characterId: string,
|
.fn()
|
||||||
|
.mockResolvedValue(new Set(options.discovered ?? [])),
|
||||||
|
isRouteOpen: (
|
||||||
|
discoveredLocationIds: ReadonlySet<string>,
|
||||||
connection: { toLocationId: string; requiresDiscovery: boolean },
|
connection: { toLocationId: string; requiresDiscovery: boolean },
|
||||||
) =>
|
) =>
|
||||||
Promise.resolve(
|
!connection.requiresDiscovery ||
|
||||||
!connection.requiresDiscovery ||
|
discoveredLocationIds.has(connection.toLocationId),
|
||||||
(options.discovered ?? []).includes(connection.toLocationId),
|
|
||||||
),
|
|
||||||
} as unknown as WorldDiscoveryService;
|
} as unknown as WorldDiscoveryService;
|
||||||
|
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
@@ -307,7 +309,11 @@ describe('WorldService', () => {
|
|||||||
find: findLocationMonsters,
|
find: findLocationMonsters,
|
||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
const worldDiscovery = {
|
const worldDiscovery = {
|
||||||
isTravelAllowed: () => Promise.resolve(true),
|
getDiscoveredLocationIds: jest.fn().mockResolvedValue(new Set()),
|
||||||
|
isRouteOpen: (
|
||||||
|
_discoveredLocationIds: ReadonlySet<string>,
|
||||||
|
connection: { requiresDiscovery: boolean },
|
||||||
|
) => !connection.requiresDiscovery,
|
||||||
discover: jest.fn(),
|
discover: jest.fn(),
|
||||||
} as unknown as WorldDiscoveryService;
|
} as unknown as WorldDiscoveryService;
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
@@ -395,7 +401,11 @@ describe('WorldService', () => {
|
|||||||
find: findLocationMonsters,
|
find: findLocationMonsters,
|
||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
const worldDiscovery = {
|
const worldDiscovery = {
|
||||||
isTravelAllowed: () => Promise.resolve(true),
|
getDiscoveredLocationIds: jest.fn().mockResolvedValue(new Set()),
|
||||||
|
isRouteOpen: (
|
||||||
|
_discoveredLocationIds: ReadonlySet<string>,
|
||||||
|
connection: { requiresDiscovery: boolean },
|
||||||
|
) => !connection.requiresDiscovery,
|
||||||
discover: jest.fn(),
|
discover: jest.fn(),
|
||||||
} as unknown as WorldDiscoveryService;
|
} as unknown as WorldDiscoveryService;
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
@@ -436,7 +446,11 @@ describe('WorldService', () => {
|
|||||||
find: findLocationMonsters,
|
find: findLocationMonsters,
|
||||||
} as unknown as Repository<LocationMonster>;
|
} as unknown as Repository<LocationMonster>;
|
||||||
const worldDiscovery = {
|
const worldDiscovery = {
|
||||||
isTravelAllowed: () => Promise.resolve(true),
|
getDiscoveredLocationIds: jest.fn().mockResolvedValue(new Set()),
|
||||||
|
isRouteOpen: (
|
||||||
|
_discoveredLocationIds: ReadonlySet<string>,
|
||||||
|
connection: { requiresDiscovery: boolean },
|
||||||
|
) => !connection.requiresDiscovery,
|
||||||
discover: jest.fn(),
|
discover: jest.fn(),
|
||||||
} as unknown as WorldDiscoveryService;
|
} as unknown as WorldDiscoveryService;
|
||||||
const service = new WorldService(
|
const service = new WorldService(
|
||||||
@@ -514,6 +528,8 @@ describe('WorldService', () => {
|
|||||||
]);
|
]);
|
||||||
expect(JSON.stringify(result)).not.toContain('Fresh bootprints');
|
expect(JSON.stringify(result)).not.toContain('Fresh bootprints');
|
||||||
expect(JSON.stringify(result)).not.toContain('Still sealed');
|
expect(JSON.stringify(result)).not.toContain('Still sealed');
|
||||||
|
expect(JSON.stringify(result)).not.toContain('discoversLocationKey');
|
||||||
|
expect(JSON.stringify(result)).not.toContain('ash-pit');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('derives the encounter preview from the location monster pool', async () => {
|
it('derives the encounter preview from the location monster pool', async () => {
|
||||||
@@ -620,7 +636,11 @@ async function loadLocation(
|
|||||||
find: jest.fn().mockResolvedValue(pool),
|
find: jest.fn().mockResolvedValue(pool),
|
||||||
} as unknown as Repository<LocationMonster>,
|
} as unknown as Repository<LocationMonster>,
|
||||||
{
|
{
|
||||||
isTravelAllowed: () => Promise.resolve(true),
|
getDiscoveredLocationIds: jest.fn().mockResolvedValue(new Set()),
|
||||||
|
isRouteOpen: (
|
||||||
|
_discoveredLocationIds: ReadonlySet<string>,
|
||||||
|
connection: { requiresDiscovery: boolean },
|
||||||
|
) => !connection.requiresDiscovery,
|
||||||
discover: jest.fn(),
|
discover: jest.fn(),
|
||||||
} as unknown as WorldDiscoveryService,
|
} as unknown as WorldDiscoveryService,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -87,15 +87,16 @@ export class WorldService {
|
|||||||
? await this.getEncounterPool(location.id)
|
? await this.getEncounterPool(location.id)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const visibleConnections: LocationConnection[] = [];
|
// Loaded once per request rather than per connection: `isRouteOpen` is
|
||||||
for (const connection of connections) {
|
// synchronous, so a location with several gated exits costs one query
|
||||||
if (
|
// here instead of one per gated connection.
|
||||||
|
const discoveredLocationIds =
|
||||||
|
await this.worldDiscovery.getDiscoveredLocationIds(characterId);
|
||||||
|
const visibleConnections = connections.filter(
|
||||||
|
(connection) =>
|
||||||
connection.enabled &&
|
connection.enabled &&
|
||||||
(await this.worldDiscovery.isTravelAllowed(characterId, connection))
|
this.worldDiscovery.isRouteOpen(discoveredLocationIds, connection),
|
||||||
) {
|
);
|
||||||
visibleConnections.push(connection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: location.id,
|
id: location.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user