Skip to content

Commit

Permalink
feat: move Contract into db.
Browse files Browse the repository at this point in the history
Also refactored the types package a little to split more out of
api.dart.
  • Loading branch information
eseidel committed Feb 18, 2024
1 parent 58bec1e commit 9c766ae
Show file tree
Hide file tree
Showing 32 changed files with 607 additions and 586 deletions.
2 changes: 1 addition & 1 deletion packages/cli/bin/contracts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void printContracts(
Future<void> command(FileSystem fs, ArgResults argResults) async {
final db = await defaultDatabase();
final printAll = argResults['all'] as bool;
final contractCache = ContractCache.load(fs)!;
final contractCache = await ContractSnapshot.load(db);
final marketPrices = await MarketPrices.load(db);
printContracts(
'completed',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/bin/deals_nearby.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Future<void> cliMain(FileSystem fs, ArgResults argResults) async {
final behaviorCache = BehaviorCache.load(fs);
final shipCache = ShipCache.load(fs)!;
final agentCache = await AgentCache.load(db);
final contractCache = ContractCache.load(fs)!;
final contractCache = await ContractSnapshot.load(db);
final centralCommand =
CentralCommand(behaviorCache: behaviorCache, shipCache: shipCache);

Expand Down
8 changes: 4 additions & 4 deletions packages/cli/lib/behavior/central_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ class CentralCommand {
/// Procurement contracts converted to sell opps.
Iterable<SellOpp> contractSellOpps(
AgentCache agentCache,
ContractCache contractCache,
ContractSnapshot contractCache,
) {
return sellOppsForContracts(
agentCache,
Expand Down Expand Up @@ -297,7 +297,7 @@ class CentralCommand {
/// Find next deal for the given [ship], considering all deals in progress.
CostedDeal? findNextDealAndLog(
AgentCache agentCache,
ContractCache contractCache,
ContractSnapshot contractCache,
MarketPrices marketPrices,
SystemsCache systemsCache,
SystemConnectivity systemConnectivity,
Expand Down Expand Up @@ -788,7 +788,7 @@ int _minimumFloatRequired(Contract contract) {
/// Procurement contracts converted to sell opps.
Iterable<SellOpp> sellOppsForContracts(
AgentCache agentCache,
ContractCache contractCache, {
ContractSnapshot contractCache, {
required int Function(Contract, TradeSymbol) remainingUnitsNeededForContract,
}) sync* {
for (final contract in affordableContracts(agentCache, contractCache)) {
Expand All @@ -815,7 +815,7 @@ Iterable<SellOpp> sellOppsForContracts(
/// complete.
Iterable<Contract> affordableContracts(
AgentCache agentCache,
ContractCache contractsCache,
ContractSnapshot contractsCache,
) {
// We should only use the contract trader when we have enough credits to
// complete the entire contract. Otherwise we're just sinking credits into a
Expand Down
45 changes: 25 additions & 20 deletions packages/cli/lib/behavior/trader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ Future<JobResult> _handleContractDealAtDestination(
const Duration(minutes: 10),
);
final neededGood = contract.goodNeeded(costedDeal.tradeSymbol);
final maybeResponse = await _deliverContractGoodsIfPossible(
final maybeContract = await _deliverContractGoodsIfPossible(
api,
db,
caches.agent,
Expand All @@ -262,14 +262,14 @@ Future<JobResult> _handleContractDealAtDestination(
);

// If we've delivered enough, complete the contract.
if (maybeResponse != null &&
maybeResponse.contract.goodNeeded(contractGood)!.amountNeeded <= 0) {
if (maybeContract != null &&
maybeContract.goodNeeded(contractGood)!.amountNeeded <= 0) {
await _completeContract(
api,
db,
caches,
ship,
maybeResponse.contract,
maybeContract,
);
}
return JobResult.complete();
Expand All @@ -285,7 +285,9 @@ Future<void> _completeContract(
final response = await api.contracts.fulfillContract(contract.id);
final data = response!.data;
await caches.agent.updateAgent(Agent.fromOpenApi(data.agent));
caches.contracts.updateContract(data.contract);
await db.upsertContract(
Contract.fromOpenApi(data.contract, DateTime.timestamp()),
);

final contactTransaction = ContractTransaction.fulfillment(
contract: contract,
Expand All @@ -301,14 +303,14 @@ Future<void> _completeContract(
shipInfo(ship, 'Contract complete!');
}

Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
Future<Contract?> _deliverContractGoodsIfPossible(
Api api,
Database db,
AgentCache agentCache,
ContractCache contractCache,
ContractSnapshot contractCache,
ShipCache shipCache,
Ship ship,
Contract contract,
Contract beforeDelivery,
ContractDeliverGood goods,
) async {
final tradeSymbol = goods.tradeSymbolObject;
Expand All @@ -323,38 +325,41 @@ Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
// Contract has already been fulfilled.","code":4504,"data":
// {"contractId":"cljysnr2wt47as60cvz377bhh"}}}
jobAssert(
!contract.fulfilled,
'Contract ${contract.id} already fulfilled.',
!beforeDelivery.fulfilled,
'Contract ${beforeDelivery.id} already fulfilled.',
const Duration(minutes: 10),
);

if (!contract.accepted) {
if (!beforeDelivery.accepted) {
shipErr(
ship,
'Contract ${contract.id} not accepted? Accepting before delivering.',
'Contract ${beforeDelivery.id} not accepted? Accepting before delivery.',
);
await acceptContractAndLog(
api,
db,
contractCache,
agentCache,
ship,
contract,
beforeDelivery,
);
}

// And we have the desired cargo.
final response = await deliverContract(
db,
api,
ship,
shipCache,
contractCache,
contract,
beforeDelivery,
tradeSymbol: tradeSymbol,
units: unitsBefore,
);
final afterDelivery =
Contract.fromOpenApi(response.contract, DateTime.timestamp());
final deliver = assertNotNull(
response.contract.goodNeeded(tradeSymbol),
afterDelivery.goodNeeded(tradeSymbol),
'No ContractDeliverGood for $tradeSymbol?',
const Duration(minutes: 10),
);
Expand All @@ -363,7 +368,7 @@ Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
'Delivered $unitsBefore ${goods.tradeSymbol} '
'to ${goods.destinationSymbol}; '
'${deliver.unitsFulfilled}/${deliver.unitsRequired}, '
'${approximateDuration(contract.timeUntilDeadline)} to deadline',
'${approximateDuration(afterDelivery.timeUntilDeadline)} to deadline',
);

// Update our cargo counts after delivering the contract goods.
Expand All @@ -372,7 +377,7 @@ Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(

// Record the delivery transaction.
final contactTransaction = ContractTransaction.delivery(
contract: contract,
contract: afterDelivery,
shipSymbol: ship.shipSymbol,
waypointSymbol: ship.waypointSymbol,
unitsDelivered: unitsDelivered,
Expand All @@ -383,7 +388,7 @@ Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
agentCache.agent.credits,
);
await db.insertTransaction(transaction);
return response;
return afterDelivery;
}

/// Handle construction deal at destination.
Expand Down Expand Up @@ -547,7 +552,7 @@ String describeExpectedContractProfit(
Future<DateTime?> acceptContractsIfNeeded(
Api api,
Database db,
ContractCache contractCache,
ContractSnapshot contractCache,
MarketPrices marketPrices,
AgentCache agentCache,
ShipCache shipCache,
Expand All @@ -557,7 +562,7 @@ Future<DateTime?> acceptContractsIfNeeded(
final contracts = contractCache.activeContracts;
if (contracts.isEmpty) {
final contract =
await negotiateContractAndLog(api, ship, shipCache, contractCache);
await negotiateContractAndLog(db, api, ship, shipCache, contractCache);
shipInfo(ship, describeExpectedContractProfit(marketPrices, contract));
return null;
}
Expand Down
10 changes: 3 additions & 7 deletions packages/cli/lib/cache/caches.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Caches {
final ShipCache ships;

/// The contract cache.
final ContractCache contracts;
ContractSnapshot contracts;

/// Known shipyard listings.
final ShipyardListingCache shipyardListings;
Expand Down Expand Up @@ -151,8 +151,7 @@ class Caches {
);
final markets = MarketCache(db, api, static.tradeGoods);
// Intentionally force refresh contracts in case we've been offline.
final contracts =
await ContractCache.loadOrFetch(api, fs: fs, forceRefresh: true);
final contracts = await fetchContracts(db, api);
final behaviors = BehaviorCache.load(fs);

final jumpGates = await JumpGateSnapshot.load(db);
Expand All @@ -168,9 +167,6 @@ class Caches {
// Make sure factions are loaded.
final factions = await loadFactions(db, api.factions);

// We rarely modify contracts, so save them out here too.
contracts.save();

return Caches(
agent: agent,
ships: ships,
Expand Down Expand Up @@ -215,8 +211,8 @@ class Caches {
// and might notice that a ship has arrived before the ship logic gets
// to run and update the status.
await ships.ensureUpToDate(api);
await contracts.ensureUpToDate(api);
await agent.ensureAgentUpToDate(api);
contracts = await contracts.ensureUpToDate(db, api);

marketListings = await MarketListingSnapshot.load(db);
}
Expand Down
98 changes: 46 additions & 52 deletions packages/cli/lib/cache/contract_cache.dart
Original file line number Diff line number Diff line change
@@ -1,67 +1,29 @@
import 'package:cli/api.dart';
import 'package:cli/cache/json_list_store.dart';
import 'package:cli/cache/response_cache.dart';
import 'package:cli/compare.dart';
import 'package:cli/logger.dart';
import 'package:cli/net/queries.dart';
import 'package:collection/collection.dart';
import 'package:file/file.dart';
import 'package:db/db.dart';
import 'package:types/types.dart';

/// In-memory cache of contacts.
class ContractCache extends ResponseListCache<Contract> {
/// Snapshot of contracts in the database.
class ContractSnapshot {
/// Creates a new contract cache.
ContractCache(
super.contracts, {
required super.fs,
super.checkEvery = 100,
super.path = defaultPath,
}) : super(refreshEntries: (Api api) => allMyContracts(api).toList());
ContractSnapshot(this.contracts);

/// Load the ContractCache from the file system.
static ContractCache? load(FileSystem fs, {String path = defaultPath}) {
final contracts = JsonListStore.loadRecords<Contract>(
fs,
path,
(j) => Contract.fromJson(j)!,
);
if (contracts != null) {
return ContractCache(contracts, fs: fs, path: path);
}
return null;
}

/// Creates a new ContractCache from the Api or FileSystem if provided.
static Future<ContractCache> loadOrFetch(
Api api, {
required FileSystem fs,
String path = defaultPath,
bool forceRefresh = false,
}) async {
if (!forceRefresh) {
final cached = load(fs, path: path);
if (cached != null) {
return cached;
}
}
final contracts = await allMyContracts(api).toList();
return ContractCache(contracts, fs: fs, path: path);
static Future<ContractSnapshot> load(Database db) async {
final contracts = await db.allContracts();
return ContractSnapshot(contracts.toList());
}

/// The default path to the contracts cache.
static const String defaultPath = 'data/contracts.json';

/// Contracts in the cache.
List<Contract> get contracts => records;
final List<Contract> contracts;

/// Updates a single contract in the cache.
void updateContract(Contract contract) {
final index = contracts.indexWhere((c) => c.id == contract.id);
if (index == -1) {
contracts.add(contract);
} else {
contracts[index] = contract;
}
save();
}
/// Number of requests between checks to ensure ships are up to date.
final int requestsBetweenChecks = 100;

int _requestsSinceLastCheck = 0;

/// Returns a list of all completed contracts.
List<Contract> get completedContracts =>
Expand All @@ -82,4 +44,36 @@ class ContractCache extends ResponseListCache<Contract> {
/// Looks up the contract by id.
Contract? contract(String id) =>
contracts.firstWhereOrNull((c) => c.id == id);

/// Fetches a new snapshot and logs if different from this one.
// TODO(eseidel): This does not belong in this class.
Future<ContractSnapshot> ensureUpToDate(Database db, Api api) async {
_requestsSinceLastCheck++;
if (_requestsSinceLastCheck < requestsBetweenChecks) {
return this;
}
_requestsSinceLastCheck = 0;

final newContracts = await fetchContracts(db, api);
final newContractsJson =
newContracts.contracts.map((c) => c.toOpenApi().toJson()).toList();
final oldContractsJson =
contracts.map((c) => c.toOpenApi().toJson()).toList();
// Our contracts class has a timestamp which we don't want to compare, so
// compare the OpenAPI JSON instead.
if (jsonMatches(newContractsJson, oldContractsJson)) {
logger.warn('Contracts changed, updating cache.');
return newContracts;
}
return this;
}
}

/// Fetches all of the user's contracts.
Future<ContractSnapshot> fetchContracts(Database db, Api api) async {
final contracts = await allMyContracts(api).toList();
for (final contract in contracts) {
await db.upsertContract(contract);
}
return ContractSnapshot(contracts);
}
1 change: 0 additions & 1 deletion packages/cli/lib/cache/ship_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class ShipCache extends ResponseListCache<Ship> {
ShipCache(
super.ships, {
required super.fs,
super.checkEvery = 100,
super.path = defaultPath,
}) : super(refreshEntries: (Api api) => allMyShips(api).toList());

Expand Down
Loading

0 comments on commit 9c766ae

Please sign in to comment.