Skip to content

Commit

Permalink
refactor: bring caches.dart to the top level
Browse files Browse the repository at this point in the history
  • Loading branch information
eseidel committed Mar 2, 2024
1 parent a06f9d8 commit e89086d
Showing 1 changed file with 257 additions and 0 deletions.
257 changes: 257 additions & 0 deletions packages/cli/lib/caches.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import 'package:cli/api.dart';
import 'package:cli/cache/agent_cache.dart';
import 'package:cli/cache/charting_cache.dart';
import 'package:cli/cache/construction_cache.dart';
import 'package:cli/cache/contract_snapshot.dart';
import 'package:cli/cache/jump_gate_snapshot.dart';
import 'package:cli/cache/market_cache.dart';
import 'package:cli/cache/market_listing_snapshot.dart';
import 'package:cli/cache/market_price_snapshot.dart';
import 'package:cli/cache/ship_snapshot.dart';
import 'package:cli/cache/static_cache.dart';
import 'package:cli/cache/systems_cache.dart';
import 'package:cli/cache/waypoint_cache.dart';
import 'package:cli/compare.dart';
import 'package:cli/logger.dart';
import 'package:cli/nav/navigation.dart';
import 'package:cli/nav/route.dart';
import 'package:cli/nav/system_connectivity.dart';
import 'package:cli/net/queries.dart';
import 'package:db/db.dart';
import 'package:file/file.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:types/types.dart';

export 'package:cli/api.dart';
export 'package:cli/cache/agent_cache.dart';
export 'package:cli/cache/behavior_snapshot.dart';
export 'package:cli/cache/charting_cache.dart';
export 'package:cli/cache/construction_cache.dart';
export 'package:cli/cache/contract_snapshot.dart';
export 'package:cli/cache/jump_gate_snapshot.dart';
export 'package:cli/cache/market_cache.dart';
export 'package:cli/cache/market_listing_snapshot.dart';
export 'package:cli/cache/market_price_snapshot.dart';
export 'package:cli/cache/ship_snapshot.dart';
export 'package:cli/cache/shipyard_listing_snapshot.dart';
export 'package:cli/cache/shipyard_price_snapshot.dart';
export 'package:cli/cache/static_cache.dart';
export 'package:cli/cache/systems_cache.dart';
export 'package:cli/cache/waypoint_cache.dart';
export 'package:cli/nav/jump_cache.dart';
export 'package:cli/nav/route.dart';
export 'package:cli/nav/system_connectivity.dart';
export 'package:file/file.dart';

/// Container for all the caches.
class Caches {
/// Creates a new cache.
@visibleForTesting
Caches({
required this.agent,
required this.marketPrices,
required this.systems,
required this.waypoints,
required this.markets,
required this.charting,
required this.routePlanner,
required this.factions,
required this.static,
required this.construction,
required this.systemConnectivity,
required this.jumpGates,
});

/// The agent cache.
final AgentCache agent;

/// The historical market price data.
MarketPriceSnapshot marketPrices;

/// The cache of systems.
final SystemsCache systems;

/// The cache of system connectivity.
final SystemConnectivity systemConnectivity;

/// The cache of construction data.
final ConstructionCache construction;

/// The cache of waypoints.
final WaypointCache waypoints;

/// The cache of jump gates.
// TODO(eseidel): This needs to update when changes!
final JumpGateSnapshot jumpGates;

/// The cache of markets.
final MarketCache markets;

/// The cache of charting data.
final ChartingCache charting;

/// The route planner.
final RoutePlanner routePlanner;

/// Cache of static data from the server.
final StaticCaches static;

/// Factions cache. Factions never change, so just holding them
/// directly as a list.
final List<Faction> factions;

/// Load the cache from disk and network.
static Future<Caches> loadOrFetch(
FileSystem fs,
Api api,
Database db, {
Future<http.Response> Function(Uri uri) httpGet = defaultHttpGet,
}) async {
// Force refresh ships and contracts in case we've been offline.
await fetchShips(db, api);
await fetchContracts(db, api);

final agent = await AgentCache.loadOrFetch(db, api);
final marketPrices = await MarketPriceSnapshot.load(db);
final systems = await SystemsCache.loadOrFetch(fs, httpGet: httpGet);
final static = StaticCaches.load(fs);
final charting = ChartingCache(db);
final construction = ConstructionCache(db);
final waypoints = WaypointCache(
api,
systems,
charting,
construction,
static.waypointTraits,
);
final markets = MarketCache(db, api, static.tradeGoods);
final jumpGates = await JumpGateSnapshot.load(db);
final constructionSnapshot = await ConstructionSnapshot.load(db);
final systemConnectivity =
SystemConnectivity.fromJumpGates(jumpGates, constructionSnapshot);
// TODO(eseidel): Find a way to avoid fetching market listings here?
final marketListings = await MarketListingSnapshot.load(db);
final routePlanner = RoutePlanner.fromSystemsCache(
systems,
systemConnectivity,
sellsFuel: defaultSellsFuel(marketListings),
);

// Make sure factions are loaded.
final factions = await loadFactions(db, api.factions);

return Caches(
agent: agent,
marketPrices: marketPrices,
systems: systems,
waypoints: waypoints,
markets: markets,
charting: charting,
static: static,
routePlanner: routePlanner,
factions: factions,
construction: construction,
systemConnectivity: systemConnectivity,
jumpGates: jumpGates,
);
}

/// Called when routing information changes (e.g. when we complete
/// a jump gate, find a new jump gate, or a jump gate breaks).
Future<void> updateRoutingCaches() async {
systemConnectivity.updateFromJumpGates(
jumpGates,
await construction.snapshot(),
);
routePlanner.clearRoutingCaches();
}
}

T _checkForChanges<T>({
required T current,
required T server,
required List<Map<String, dynamic>> Function(T) toJsonList,
}) {
final currentJson = toJsonList(current);
final serverJson = toJsonList(server);
if (!jsonMatches(currentJson, serverJson)) {
logger.warn('$T changed, updating cache.');
return server;
}
return current;
}

/// Class to hold state for updating caches at the top of the loop.
class TopOfLoopUpdater {
/// Number of requests between checks to ensure ships are up to date.
final int requestsBetweenChecks = 100;

int _requestsSinceLastCheck = 0;

/// Update the caches at the top of the loop.
Future<void> updateAtTopOfLoop(Caches caches, Database db, Api api) async {
// MarketCache only live for one loop over the ships.
caches.markets.resetForLoop();

// The ships check races with the code in continueNavigationIfNeeded which
// knows how to update the ShipNavStatus from IN_TRANSIT to IN_ORBIT when
// a ship has arrived. We could add some special logic here to ignore
// that false positive. This check is called at the top of every loop
// and might notice that a ship has arrived before the ship logic gets
// to run and update the status.
_requestsSinceLastCheck++;
if (_requestsSinceLastCheck >= requestsBetweenChecks) {
_requestsSinceLastCheck = 0;
// This does not need to assign to anything, fetchContracts updates
// the db already.
_checkForChanges(
current: await ContractSnapshot.load(db),
server: await fetchContracts(db, api),
// Use OpenAPI's toJson to restrict to only the fields the server sends.
toJsonList: (e) =>
e.contracts.map((e) => e.toOpenApi().toJson()).toList(),
);
_checkForChanges(
current: await ShipSnapshot.load(db)
..updateForServerTime(DateTime.timestamp()),
server: await fetchShips(db, api),
toJsonList: (e) => e.ships.map((e) => e.toJson()).toList(),
);
// caches.agent should be deleted.
await caches.agent.updateAgent(
_checkForChanges(
current: caches.agent.agent,
server: await getMyAgent(api),
toJsonList: (e) => [e.toOpenApi().toJson()],
),
);
}

// Should all be deleted from Caches.
caches.marketPrices = await MarketPriceSnapshot.load(db);
}
}

/// Load all factions from the API.
// With our out-of-process rate limiting, this won't matter that it uses
// a separate API client.
Future<List<Faction>> allFactions(FactionsApi factionsApi) async {
final factions = await fetchAllPages(factionsApi, (factionsApi, page) async {
final response = await factionsApi.getFactions(page: page);
return (response!.data, response.meta);
}).toList();
return factions;
}

/// Loads the factions from the database, or fetches them from the API if
/// they're not cached.
Future<List<Faction>> loadFactions(Database db, FactionsApi factionsApi) async {
final cachedFactions = await db.allFactions();
if (cachedFactions.isNotEmpty) {
return Future.value(cachedFactions.toList());
}
final factions = await allFactions(factionsApi);
await db.cacheFactions(factions);
return factions;
}

0 comments on commit e89086d

Please sign in to comment.