refactor(#27): Simplify appraisal

This commit is contained in:
Sirttas
2026-07-15 18:39:40 +02:00
parent 0d61826c70
commit 24c7ec4917
8 changed files with 14 additions and 136 deletions
-35
View File
@@ -1,35 +0,0 @@
import { describe, expect, test } from 'vitest'
import { RegionalMarketCache } from './RegionalMarketCache'
describe('RegionalMarketCache', () => {
test('should cache and retrieve values', async () => {
const cache = new RegionalMarketCache<string>(1000)
cache.set(1, 1, 'test')
expect(cache.get(1, 1)).toBe('test')
})
test('should remove values', async () => {
const cache = new RegionalMarketCache<string>(1000)
cache.set(1, 1, 'test')
cache.remove(1, 1)
expect(cache.get(1, 1)).toBeUndefined()
})
test('should compute values if absent', async () => {
const cache = new RegionalMarketCache<string>(1000)
const value = await cache.computeIfAbsent(1, 1, () => Promise.resolve('test'))
expect(value).toBe('test')
expect(cache.get(1, 1)).toBe('test')
})
test('should expire values', async () => {
const cache = new RegionalMarketCache<string>(1)
cache.set(1, 1, 'test')
await new Promise(resolve => setTimeout(resolve, 10))
expect(cache.get(1, 1)).toBeUndefined()
})
})
-50
View File
@@ -1,50 +0,0 @@
class CacheEntry<T> {
public value: T;
public expiration: Date;
constructor(value: T, expiration: Date) {
this.value = value;
this.expiration = expiration;
}
}
export type ExpirationSupplier<T> = (v: T) => Date;
export class RegionalMarketCache<T> {
private cache: Record<number, Record<number, CacheEntry<T>>>;
private expirationSupplier: (v: T) => Date;
constructor(expiration: ExpirationSupplier<T> | number) {
this.cache = {};
this.expirationSupplier = expiration instanceof Function ? expiration : () => new Date(Date.now() + expiration);
}
public get(regionId: number, typeId: number): T | undefined {
const entry = this.cache[regionId]?.[typeId];
if (entry && entry.expiration > new Date()) {
return entry.value;
}
this.remove(regionId, typeId);
return undefined;
}
public set(regionId: number, typeId: number, value: T): void {
this.cache[regionId] = this.cache[regionId] ?? {};
this.cache[regionId][typeId] = new CacheEntry(value, this.expirationSupplier(value));
}
public remove(regionId: number, typeId: number): void {
delete this.cache[regionId]?.[typeId];
}
public async computeIfAbsent(regionId: number, typeId: number, supplier: () => (Promise<T> | T)): Promise<T> {
let value = this.get(regionId, typeId);
if (!value) {
value = await supplier();
this.set(regionId, typeId, value);
}
return value;
}
};
+2 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import {getMarketTypes, MarketTypePrice, useAppraisalStore} from "@/market";
import {getMarketTypes, getPrices, MarketTypePrice} from "@/market";
import {ref, watch} from 'vue';
import {ArrowPathIcon} from '@heroicons/vue/24/outline';
import {useAutoRefresh} from '@/composables';
@@ -20,7 +20,6 @@ const props = defineProps<Props>();
const buyModal = ref<typeof BuyModal>();
const sellModal = ref<typeof SellModal>();
const appraisalStore = useAppraisalStore();
const enriched = ref<AcquiredType[]>([]);
const refresh = async (itms: RawAcquiredType[] = props.items) => {
@@ -30,7 +29,7 @@ const refresh = async (itms: RawAcquiredType[] = props.items) => {
}
const types = await getMarketTypes([...new Set(itms.map(i => i.type))]);
const prices = await appraisalStore.getPrices(types);
const prices = await getPrices(types);
enriched.value = itms.map(i => {
const price = prices.find(p => p.type.id === i.type) as MarketTypePrice;
+6 -36
View File
@@ -1,47 +1,17 @@
import {defineStore} from 'pinia';
import {RegionalMarketCache} from '../RegionalMarketCache';
import {jitaId} from '../market';
import {MarketType} from "../type";
import {MarketTypePrice} from './MarketTypePrice';
import {getMammonPrices} from './mammon';
const CACHE_DURATION = 1000 * 60 * 5; // 5 minutes
const BATCH_SIZE = 100;
export const useAppraisalStore = defineStore('appraisal', () => {
const cache: RegionalMarketCache<MarketTypePrice> = new RegionalMarketCache(CACHE_DURATION);
export const getPrice = async (type: MarketType, locationId?: number): Promise<MarketTypePrice> => (await getPrices([type], locationId))[0];
const getPricesUncached = getMammonPrices;
const getPrice = async (type: MarketType, locationId?: number): Promise<MarketTypePrice> => (await getPrices([type], locationId))[0];
const getPrices = async (types: MarketType[], locationId?: number): Promise<MarketTypePrice[]> => {
const cached: MarketTypePrice[] = [];
const uncached: MarketType[] = [];
const lId = locationId ?? jitaId;
types.forEach(t => {
const cachedPrice = cache.get(lId, t.id);
if (cachedPrice) {
cached.push(cachedPrice);
} else {
uncached.push(t);
}
});
if (uncached.length > 0) {
export const getPrices = async (types: MarketType[], locationId?: number): Promise<MarketTypePrice[]> => {
const batches: Promise<MarketTypePrice[]>[] = [];
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE), lId));
for (let i = 0; i < types.length; i += BATCH_SIZE) {
batches.push(getMammonPrices(types.slice(i, i + BATCH_SIZE), locationId));
}
const prices = (await Promise.all(batches)).flat();
prices.forEach(p => cache.set(lId, p.type.id, p));
return [ ...cached, ...prices ];
}
return cached;
};
return { getPrice, getPrices };
});
return (await Promise.all(batches)).flat();
};
-2
View File
@@ -1,4 +1,3 @@
export * from './RegionalMarketCache';
export * from './history';
export * from './location';
export * from './order';
@@ -6,7 +5,6 @@ export * from './tax';
export * from './type';
export * from './appraisal';
export * from './market';
export { default as IskLabel } from './IskLabel.vue';
-1
View File
@@ -1 +0,0 @@
export const jitaId = 60003760;
+2 -4
View File
@@ -1,11 +1,9 @@
<script setup lang="ts">
import {ref} from 'vue';
import {Dropdown} from '@/components';
import {compareAppraisal, ComparedItem, CompareResultTable, useAppraisalStore} from '@/market/appraisal';
import {compareAppraisal, ComparedItem, CompareResultTable, getPrices} from '@/market/appraisal';
import {MarketLocation, MarketLocationInput} from '@/market/location';
const store = useAppraisalStore();
const items = ref('');
const from = ref<MarketLocation>();
const to = ref<MarketLocation>();
@@ -19,7 +17,7 @@ const send = async () => {
const selected = [from.value, to.value].filter((l): l is MarketLocation => l !== undefined);
loading.value = true;
try {
result.value = await compareAppraisal(items.value, selected.map(l => l.id), store.getPrices);
result.value = await compareAppraisal(items.value, selected.map(l => l.id), getPrices);
resultLocations.value = selected;
} finally {
loading.value = false;
+2 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import {ClipboardButton} from '@/components';
import {getMarketType, MarketType, MarketTypeInput, useAppraisalStore, useMarketTaxStore} from "@/market";
import {getMarketType, getPrice, MarketType, MarketTypeInput, useMarketTaxStore} from "@/market";
import {AcquisitionResultTable, BuyModal} from '@/market/acquisition';
import {ScanResultTable, toScanResult} from '@/market/scan';
import {acquisitionApi, marketApi} from "@/mammon";
@@ -18,10 +18,9 @@ const router = useRouter();
const item = ref<MarketType>();
const inputItem = ref<MarketType>();
const appraisalStore = useAppraisalStore();
const marketTaxStore = useMarketTaxStore();
const days = useStorage('market-scan-days', 365);
const price = computedAsync(() => item.value ? appraisalStore.getPrice(item.value) : undefined);
const price = computedAsync(() => item.value ? getPrice(item.value) : undefined);
const result = computedAsync(async () => {
if (!item.value) {
return undefined;