From d9bf419f1ca49697602b72989351bb987a9fa86c Mon Sep 17 00:00:00 2001 From: Step7750 Date: Wed, 8 Apr 2026 17:51:06 -0600 Subject: [PATCH 01/12] Implements Support for Rank Fetching via FloatDB Gateway --- src/environment.dev.ts | 1 + src/environment.staging.ts | 1 + src/environment.ts | 1 + src/lib/bridge/handlers/fetch_inspect_info.ts | 59 ++++++---- src/lib/services/rank_batcher.ts | 79 +++++++++++++ src/lib/services/threshold_fetcher.ts | 107 ++++++++++++++++++ 6 files changed, 228 insertions(+), 20 deletions(-) create mode 100644 src/lib/services/rank_batcher.ts create mode 100644 src/lib/services/threshold_fetcher.ts diff --git a/src/environment.dev.ts b/src/environment.dev.ts index a0677260..d715f4ec 100644 --- a/src/environment.dev.ts +++ b/src/environment.dev.ts @@ -6,4 +6,5 @@ export const environment = { loggingLevel: 'Error', }, reverse_watch_base_api_url: 'http://localhost:3434/api', + floatdb_gateway_url: 'https://gateway.floatdb.com', }; diff --git a/src/environment.staging.ts b/src/environment.staging.ts index adf33364..a35422eb 100644 --- a/src/environment.staging.ts +++ b/src/environment.staging.ts @@ -6,4 +6,5 @@ export const environment = { loggingLevel: 'Error', }, reverse_watch_base_api_url: 'https://reverse.watch/api', + floatdb_gateway_url: 'https://gateway.floatdb.com', }; diff --git a/src/environment.ts b/src/environment.ts index 02f83b4e..a81c07ec 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -6,4 +6,5 @@ export const environment = { loggingLevel: 'Warn', }, reverse_watch_base_api_url: 'https://reverse.watch/api', + floatdb_gateway_url: 'https://gateway.floatdb.com', }; diff --git a/src/lib/bridge/handlers/fetch_inspect_info.ts b/src/lib/bridge/handlers/fetch_inspect_info.ts index 214e7ff7..c6965082 100644 --- a/src/lib/bridge/handlers/fetch_inspect_info.ts +++ b/src/lib/bridge/handlers/fetch_inspect_info.ts @@ -2,6 +2,8 @@ import {decodeLink, CEconItemPreviewDataBlock} from '@csfloat/cs2-inspect-serial import {SimpleHandler} from './main'; import {RequestType} from './types'; import {gSchemaFetcher} from '../../services/schema_fetcher'; +import {gThresholdFetcher} from '../../services/threshold_fetcher'; +import {gRankBatcher} from '../../services/rank_batcher'; import type {ItemSchema} from '../../types/schema'; interface Sticker { @@ -110,27 +112,44 @@ export const FetchInspectInfo = new SimpleHandler; +} + +/** + * Batches concurrent rank check requests into a single POST to /v1/ranks/check. + * Callers receive individual promises; a short debounce window (50ms) collects + * qualifying items so that bulk page loads produce one network request instead of many. + */ +class RankBatcher { + private pending = new Map(); + private timer: ReturnType | null = null; + + check(link: string, assetId: string): Promise { + const existing = this.pending.get(assetId); + if (existing) { + return existing.deferred.promise(); + } + + const deferred = new DeferredPromise(); + this.pending.set(assetId, {link, deferred}); + + if (this.timer !== null) { + clearTimeout(this.timer); + } + this.timer = setTimeout(() => this.flush(), DEBOUNCE_MS); + + return deferred.promise(); + } + + private async flush(): Promise { + this.timer = null; + + const batch = new Map(this.pending); + this.pending.clear(); + + if (batch.size === 0) return; + + const links = Array.from(batch.values()).map((item) => item.link); + + try { + const resp = await fetch(RANKS_CHECK_URL, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({links}), + }); + + if (!resp.ok) { + throw new Error(`Ranks check failed with status ${resp.status}`); + } + + const data: {ranks: Record} = await resp.json(); + + for (const [assetId, item] of batch) { + const rank = data.ranks[assetId] ?? null; + item.deferred.resolve(rank); + } + } catch (error) { + console.error('Failed to batch check ranks:', error); + for (const [, item] of batch) { + item.deferred.resolve(null); + } + } + } +} + +export const gRankBatcher = new RankBatcher(); diff --git a/src/lib/services/threshold_fetcher.ts b/src/lib/services/threshold_fetcher.ts new file mode 100644 index 00000000..42d94942 --- /dev/null +++ b/src/lib/services/threshold_fetcher.ts @@ -0,0 +1,107 @@ +import {environment} from '../../environment'; + +const THRESHOLDS_URL = `${environment.floatdb_gateway_url}/v1/ranks/thresholds/bin`; +const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour + +interface ThresholdEntry { + low: number; + high: number; +} + +function makeKey(defindex: number, paintindex: number, stattrak: boolean, souvenir: boolean): string { + return `${defindex}:${paintindex}:${stattrak ? 1 : 0}:${souvenir ? 1 : 0}`; +} + +/** + * Binary wire format (little-endian): + * Header (4 bytes): version(u8), entry_size(u8), count(u16) + * Per entry (13 bytes): defindex(u16), paintindex(u16), flags(u8), low(f32), high(f32) + */ +function parseThresholdsBinary(buffer: ArrayBuffer): Map { + const view = new DataView(buffer); + const entrySize = view.getUint8(1); + const count = view.getUint16(2, true); + const headerSize = 4; + + const map = new Map(); + + for (let i = 0; i < count; i++) { + const off = headerSize + i * entrySize; + const defindex = view.getUint16(off, true); + const paintindex = view.getUint16(off + 2, true); + const flags = view.getUint8(off + 4); + const stattrak = (flags & 1) !== 0; + const souvenir = (flags & 2) !== 0; + const low = view.getFloat32(off + 5, true); + const high = view.getFloat32(off + 9, true); + + map.set(makeKey(defindex, paintindex, stattrak, souvenir), {low, high}); + } + + return map; +} + +class ThresholdFetcher { + private thresholds: Map | null = null; + private lastFetched = 0; + private pendingFetch?: Promise>; + + async qualifiesForRankCheck( + defindex: number, + paintindex: number, + stattrak: boolean, + souvenir: boolean, + floatvalue: number + ): Promise { + const thresholds = await this.getThresholds(); + const entry = thresholds.get(makeKey(defindex, paintindex, stattrak, souvenir)); + if (!entry) return false; + return floatvalue <= entry.low || floatvalue >= entry.high; + } + + private async getThresholds(): Promise> { + if (this.thresholds && Date.now() - this.lastFetched < CACHE_DURATION_MS) { + return this.thresholds; + } + + if (this.pendingFetch) { + return this.pendingFetch; + } + + const fetchPromise = this.fetchThresholds(); + this.pendingFetch = fetchPromise; + + try { + return await fetchPromise; + } finally { + if (this.pendingFetch === fetchPromise) { + this.pendingFetch = undefined; + } + } + } + + private async fetchThresholds(): Promise> { + try { + const resp = await fetch(THRESHOLDS_URL); + if (!resp.ok) { + throw new Error(`Thresholds request failed with status ${resp.status}`); + } + + const buffer = await resp.arrayBuffer(); + const parsed = parseThresholdsBinary(buffer); + + this.thresholds = parsed; + this.lastFetched = Date.now(); + + return parsed; + } catch (error) { + if (this.thresholds) { + console.error('Error fetching thresholds, using cached:', error); + return this.thresholds; + } + throw error; + } + } +} + +export const gThresholdFetcher = new ThresholdFetcher(); From 5859c460b606960f75e092ed0f424df745a011f3 Mon Sep 17 00:00:00 2001 From: Step7750 Date: Wed, 8 Apr 2026 23:47:02 -0600 Subject: [PATCH 02/12] Improves Rank Batching Logic Prevents duplicate in-flight or cached requests from re-requesting ranks. Refactors the float fetcher service for performance. --- src/lib/bridge/handlers/fetch_inspect_info.ts | 216 ++++++++++-------- src/lib/bridge/handlers/handlers.ts | 3 +- src/lib/bridge/handlers/types.ts | 1 + .../components/common/item_holder_metadata.ts | 3 +- .../inventory/selected_item_info.ts | 1 + src/lib/components/market/item_row_wrapper.ts | 1 + src/lib/components/market/sort_listings.ts | 2 +- src/lib/page_scripts/inventory.ts | 69 +++++- src/lib/services/float_fetcher.ts | 94 ++++++-- src/lib/services/rank_batcher.ts | 17 +- src/lib/types/steam.d.ts | 2 +- 11 files changed, 290 insertions(+), 119 deletions(-) diff --git a/src/lib/bridge/handlers/fetch_inspect_info.ts b/src/lib/bridge/handlers/fetch_inspect_info.ts index c6965082..f985efcb 100644 --- a/src/lib/bridge/handlers/fetch_inspect_info.ts +++ b/src/lib/bridge/handlers/fetch_inspect_info.ts @@ -44,6 +44,7 @@ export interface ItemInfo { export interface FetchInspectInfoRequest { link: string; + asset_id: string; listPrice?: number; } @@ -52,104 +53,139 @@ export interface FetchInspectInfoResponse { error?: string; } +async function processInspectItem( + req: FetchInspectInfoRequest, + schema: ItemSchema.Response +): Promise { + let decoded: CEconItemPreviewDataBlock; + try { + decoded = decodeLink(req.link); + } catch (error) { + throw new Error('Failed to decode inspect link'); + } + + const defindex = decoded.defindex ?? 0; + const paintindex = decoded.paintindex ?? 0; + const floatvalue = decoded.paintwear ?? 0; + + let min = 0; + let max = 1; + let weaponType: string | undefined; + let itemName: string | undefined; + let rarityName: string | undefined; + let stickers: Sticker[] = []; + let keychains: Keychain[] = []; + + try { + const weapon = schema.weapons[defindex]; + const paint = getSchemaPaint(weapon, paintindex); + + weaponType = weapon?.name; + rarityName = schema.rarities.find((rarity) => rarity.value === (paint?.rarity ?? decoded.rarity))?.name; + + if (paint) { + itemName = paint.name; + min = paint.min; + max = paint.max; + } + + stickers = decoded.stickers.map((sticker) => { + const schemaSticker = schema.stickers[sticker.stickerId?.toString() ?? '']; + return { + slot: sticker.slot ?? 0, + stickerId: sticker.stickerId ?? 0, + wear: sticker.wear, + name: schemaSticker?.market_hash_name, + }; + }); + + keychains = decoded.keychains.map((keychain) => { + const schemaKeychain = schema.keychains[keychain.stickerId?.toString() ?? '']; + return { + slot: keychain.slot ?? 0, + stickerId: keychain.stickerId ?? 0, + wear: keychain.wear, + pattern: keychain.pattern ?? 0, + name: schemaKeychain?.market_hash_name, + }; + }); + } catch (error) { + console.error('Failed to fetch schema item metadata:', error); + } + + const iteminfo: ItemInfo = { + stickers, + keychains, + itemid: decoded.itemid?.toString() ?? '', + defindex, + paintindex, + rarity: decoded.rarity ?? 0, + quality: decoded.quality ?? 0, + paintseed: decoded.paintseed ?? 0, + inventory: decoded.inventory ?? 0, + origin: decoded.origin ?? 0, + floatvalue, + min, + max, + weapon_type: weaponType, + item_name: itemName, + rarity_name: rarityName, + wear_name: getWearName(floatvalue), + }; + + try { + if (decoded.itemid != null && decoded.paintwear != null) { + const stattrak = decoded.killeaterscoretype !== undefined; + const souvenir = (decoded.quality ?? 0) === 12; + + if (await gThresholdFetcher.qualifiesForRankCheck(defindex, paintindex, stattrak, souvenir, floatvalue)) { + const rankResult = await gRankBatcher.check(req.link, decoded.itemid.toString()); + if (rankResult) { + iteminfo.low_rank = rankResult.low_rank; + iteminfo.high_rank = rankResult.high_rank; + } + } + } + } catch (e) { + console.error('Failed to check rank:', e); + } + + return iteminfo; +} + export const FetchInspectInfo = new SimpleHandler( RequestType.FETCH_INSPECT_INFO, async (req) => { - let decoded: CEconItemPreviewDataBlock; - try { - decoded = decodeLink(req.link); - } catch (error) { - throw new Error('Failed to decode inspect link'); - } + const schema = await gSchemaFetcher.getSchema(); + return {iteminfo: await processInspectItem(req, schema)}; + } +); - const defindex = decoded.defindex ?? 0; - const paintindex = decoded.paintindex ?? 0; - const floatvalue = decoded.paintwear ?? 0; - - let min = 0; - let max = 1; - let weaponType: string | undefined; - let itemName: string | undefined; - let rarityName: string | undefined; - let stickers: Sticker[] = []; - let keychains: Keychain[] = []; - - try { - const schema = await gSchemaFetcher.getSchema(); - const weapon = schema.weapons[defindex]; - const paint = getSchemaPaint(weapon, paintindex); - - weaponType = weapon?.name; - rarityName = schema.rarities.find((rarity) => rarity.value === (paint?.rarity ?? decoded.rarity))?.name; - - if (paint) { - itemName = paint.name; - min = paint.min; - max = paint.max; - } +export interface FetchInspectInfoBatchRequest { + requests: FetchInspectInfoRequest[]; +} - stickers = decoded.stickers.map((sticker) => { - const schemaSticker = schema.stickers[sticker.stickerId?.toString() ?? '']; - return { - slot: sticker.slot ?? 0, - stickerId: sticker.stickerId ?? 0, - wear: sticker.wear, - name: schemaSticker?.market_hash_name, - }; - }); - - keychains = decoded.keychains.map((keychain) => { - const schemaKeychain = schema.keychains[keychain.stickerId?.toString() ?? '']; - return { - slot: keychain.slot ?? 0, - stickerId: keychain.stickerId ?? 0, - wear: keychain.wear, - pattern: keychain.pattern ?? 0, - name: schemaKeychain?.market_hash_name, - }; - }); - } catch (error) { - console.error('Failed to fetch schema item metadata:', error); - } +export interface FetchInspectInfoBatchResponse { + results: Record; +} - const iteminfo: ItemInfo = { - stickers, - keychains, - itemid: decoded.itemid?.toString() ?? '', - defindex, - paintindex, - rarity: decoded.rarity ?? 0, - quality: decoded.quality ?? 0, - paintseed: decoded.paintseed ?? 0, - inventory: decoded.inventory ?? 0, - origin: decoded.origin ?? 0, - floatvalue, - min, - max, - weapon_type: weaponType, - item_name: itemName, - rarity_name: rarityName, - wear_name: getWearName(floatvalue), - }; - - try { - if (decoded.itemid != null) { - const stattrak = decoded.killeaterscoretype !== undefined; - const souvenir = (decoded.quality ?? 0) === 12; - - if (await gThresholdFetcher.qualifiesForRankCheck(defindex, paintindex, stattrak, souvenir, floatvalue)) { - const rankResult = await gRankBatcher.check(req.link, decoded.itemid.toString()); - if (rankResult) { - iteminfo.low_rank = rankResult.low_rank; - iteminfo.high_rank = rankResult.high_rank; - } +export const FetchInspectInfoBatch = new SimpleHandler( + RequestType.FETCH_INSPECT_INFO_BATCH, + async (req) => { + const schema = await gSchemaFetcher.getSchema(); + + const results: Record = {}; + await Promise.all( + req.requests.map(async (itemReq) => { + try { + results[itemReq.asset_id] = {iteminfo: await processInspectItem(itemReq, schema)}; + } catch (e) { + results[itemReq.asset_id] = {iteminfo: {} as ItemInfo, error: (e as any).toString()}; } - } - } catch (e) { - console.error('Failed to check rank:', e); - } + }) + ); - return {iteminfo}; + return {results}; } ); diff --git a/src/lib/bridge/handlers/handlers.ts b/src/lib/bridge/handlers/handlers.ts index 1b679ac0..e868a0c6 100644 --- a/src/lib/bridge/handlers/handlers.ts +++ b/src/lib/bridge/handlers/handlers.ts @@ -1,6 +1,6 @@ import {ExecuteScriptOnPage} from './execute_script'; import {FetchStall} from './fetch_stall'; -import {FetchInspectInfo} from './fetch_inspect_info'; +import {FetchInspectInfo, FetchInspectInfoBatch} from './fetch_inspect_info'; import {ExecuteCssOnPage} from './execute_css'; import {StorageGet} from './storage_get'; import {StorageSet} from './storage_set'; @@ -78,4 +78,5 @@ export const HANDLERS_MAP: {[key in RequestType]: RequestHandler} = { [RequestType.FETCH_NOTARY_TOKEN]: FetchNotaryToken, [RequestType.FETCH_STEAM_POWERED_INVENTORY]: FetchSteamPoweredInventory, [RequestType.FETCH_REVERSAL_STATUS]: FetchReversalStatus, + [RequestType.FETCH_INSPECT_INFO_BATCH]: FetchInspectInfoBatch, }; diff --git a/src/lib/bridge/handlers/types.ts b/src/lib/bridge/handlers/types.ts index 6c9285b2..22e62bee 100644 --- a/src/lib/bridge/handlers/types.ts +++ b/src/lib/bridge/handlers/types.ts @@ -37,4 +37,5 @@ export enum RequestType { FETCH_NOTARY_TOKEN = 35, FETCH_STEAM_POWERED_INVENTORY = 36, FETCH_REVERSAL_STATUS = 37, + FETCH_INSPECT_INFO_BATCH = 38, } diff --git a/src/lib/components/common/item_holder_metadata.ts b/src/lib/components/common/item_holder_metadata.ts index 261ac510..d75f79d7 100644 --- a/src/lib/components/common/item_holder_metadata.ts +++ b/src/lib/components/common/item_holder_metadata.ts @@ -173,11 +173,12 @@ export abstract class ItemHolderMetadata extends FloatElement { if (!isSkin(this.asset) && !isCharm(this.asset)) return; // Commodities won't have inspect links - if (!this.inspectLink) return; + if (!this.inspectLink || !this.assetId) return; try { this.itemInfo = await gFloatFetcher.fetch({ link: this.inspectLink, + asset_id: this.assetId, }); } catch (e: any) { console.error(`Failed to fetch float for ${this.assetId}: ${e.toString()}`); diff --git a/src/lib/components/inventory/selected_item_info.ts b/src/lib/components/inventory/selected_item_info.ts index 35ece6f8..a76bf0c6 100644 --- a/src/lib/components/inventory/selected_item_info.ts +++ b/src/lib/components/inventory/selected_item_info.ts @@ -262,6 +262,7 @@ export class SelectedItemInfo extends FloatElement { ) { try { this.itemInfo = await gFloatFetcher.fetch({ + asset_id: this.asset.assetid, link: this.inspectLink, }); } catch (e: any) { diff --git a/src/lib/components/market/item_row_wrapper.ts b/src/lib/components/market/item_row_wrapper.ts index bc94cb19..35983135 100644 --- a/src/lib/components/market/item_row_wrapper.ts +++ b/src/lib/components/market/item_row_wrapper.ts @@ -85,6 +85,7 @@ export class ItemRowWrapper extends FloatElement { async fetchFloat(): Promise { return gFloatFetcher.fetch({ link: this.inspectLink!, + asset_id: this.listingInfo?.asset.id!, listPrice: this.usdPrice, }); } diff --git a/src/lib/components/market/sort_listings.ts b/src/lib/components/market/sort_listings.ts index d063481f..dce0a0b2 100644 --- a/src/lib/components/market/sort_listings.ts +++ b/src/lib/components/market/sort_listings.ts @@ -122,9 +122,9 @@ export class SortListings extends FloatElement { // Catch error to prevent one failure from stopping the Promise.all() later try { const link = getMarketInspectLink(listingId); - const info = await gFloatFetcher.fetch({link: link!}); const listingInfo = g_rgListingInfo[listingId]; const asset = g_rgAssets[AppId.CSGO][ContextId.PRIMARY][listingInfo.asset.id]; + const info = await gFloatFetcher.fetch({link: link!, asset_id: listingInfo.asset.id}); return { failed: false, info, diff --git a/src/lib/page_scripts/inventory.ts b/src/lib/page_scripts/inventory.ts index 0809ad07..4df78bf0 100644 --- a/src/lib/page_scripts/inventory.ts +++ b/src/lib/page_scripts/inventory.ts @@ -1,7 +1,74 @@ import {init} from './utils'; import '../components/inventory/inventory_item_holder_metadata'; import '../components/inventory/selected_item_info'; +import {Observe} from '../utils/observers'; +import {gFloatFetcher} from '../services/float_fetcher'; +import {rgAssetProperty} from '../types/steam'; +import {isCAppwideInventory} from '../utils/checkers'; +import {ContextId} from '../types/steam_constants'; init('src/lib/page_scripts/inventory.js', main); -async function main() {} +async function main() { + let initialRun = false; + + /** + * We want to limit the number of rank checks we make to the FloatDB gateway. If we wait until each item is + * rendered in an inventory (requires them to click on each page), then it causes a separate server-side fetch + * for each page. + * + * Instead, we eagerly fetch the ranks for all items that have been loaded. + */ + Observe(() => { + const allProps = getAllCS2AssetProperties(); + const propsLength = Object.keys(allProps).length; + if (propsLength > 0 && !initialRun) { + // Ensure that we run the function at least once if we happened to load + // and all the properties were already there. + initialRun = true; + return true; + } + return Object.keys(allProps).length; + }, () => { + if (typeof g_ActiveInventory === 'undefined') return; + + const assetProperties = getAllCS2AssetProperties(); + + for (const [asset_id, props] of Object.entries(assetProperties)) { + if (!props.find(e => e.propertyid === 2)) { + // doesn't have a float, skip + continue; + } + + const serializedLink = props.find(e => e.propertyid === 6); + if (!serializedLink?.string_value) { + continue; + } + + gFloatFetcher.fetch({ + asset_id: asset_id, + link: `steam://run/730//+csgo_econ_action_preview%20${serializedLink.string_value}` + }); + } + }) +} + +function getAllCS2AssetProperties(): {[assetId: string]: rgAssetProperty[]} { + if (typeof g_ActiveInventory === 'undefined') return {}; + + const allProperties = g_ActiveInventory.m_rgAssetProperties || {}; + + if (isCAppwideInventory(g_ActiveInventory)) { + const primaryProps = g_ActiveInventory.m_rgChildInventories[ContextId.PRIMARY]?.m_rgAssetProperties; + if (Object.keys(primaryProps).length > 0) { + Object.assign(allProperties, primaryProps); + } + + const protectedProps = g_ActiveInventory.m_rgChildInventories[ContextId.PROTECTED]?.m_rgAssetProperties; + if (Object.keys(protectedProps).length > 0) { + Object.assign(allProperties, protectedProps); + } + } + + return allProperties; +} diff --git a/src/lib/services/float_fetcher.ts b/src/lib/services/float_fetcher.ts index 740b2e87..33ec1fea 100644 --- a/src/lib/services/float_fetcher.ts +++ b/src/lib/services/float_fetcher.ts @@ -1,26 +1,86 @@ -import {Job, SimpleCachedQueue} from '../utils/queue'; import {ClientSend} from '../bridge/client'; -import {FetchInspectInfo, FetchInspectInfoRequest, ItemInfo} from '../bridge/handlers/fetch_inspect_info'; +import {FetchInspectInfoBatch, FetchInspectInfoRequest, ItemInfo} from '../bridge/handlers/fetch_inspect_info'; +import {Cache} from '../utils/cache'; +import {DeferredPromise} from '../utils/deferred_promise'; -class InspectJob extends Job { - hashCode(): string { - return this.data.link; - } -} - -class FloatFetcher extends SimpleCachedQueue { - constructor() { - /** allow up to 10 simultaneous float fetch reqs */ - super(10); - } +/** + * All fetch() calls within a single microtask are collected and sent to the service worker as one bridge message. + * + * Cached requests and in-flight requests are de-duped. + * + * Why? Because sending hundreds of messages has transport overhead of hundreds of milliseconds and causes sub-optimal + * rank check batching. + */ +class FloatFetcher { + private cache = new Cache(); + private pending = new Map}>(); + private inFlight = new Map>(); + private flushScheduled = false; fetch(req: FetchInspectInfoRequest): Promise { - return this.add(new InspectJob(req)); + const key = req.asset_id; + + if (this.cache.has(key)) { + return Promise.resolve(this.cache.getOrThrow(key)); + } + + const inflight = this.inFlight.get(key); + if (inflight) { + return inflight.promise(); + } + + const existing = this.pending.get(key); + if (existing) { + return existing.deferred.promise(); + } + + const deferred = new DeferredPromise(); + this.pending.set(key, {req, deferred}); + + if (!this.flushScheduled) { + this.flushScheduled = true; + queueMicrotask(() => this.flush()); + } + + return deferred.promise(); } - protected async process(req: FetchInspectInfoRequest): Promise { - const resp = await ClientSend(FetchInspectInfo, req); - return resp.iteminfo; + private async flush() { + this.flushScheduled = false; + + const batch = new Map(this.pending); + this.pending.clear(); + + if (batch.size === 0) return; + + for (const [assetId, {deferred}] of batch) { + this.inFlight.set(assetId, deferred); + } + + try { + const requests = Array.from(batch.values()).map((e) => e.req); + const resp = await ClientSend(FetchInspectInfoBatch, {requests}); + + for (const [assetId, {deferred}] of batch) { + const result = resp.results[assetId]; + if (result?.error) { + deferred.reject(result.error); + } else if (result?.iteminfo) { + this.cache.set(assetId, result.iteminfo); + deferred.resolve(result.iteminfo); + } else { + deferred.reject('No result for ' + assetId); + } + } + } catch (e) { + for (const [, {deferred}] of batch) { + deferred.reject((e as any).toString()); + } + } finally { + for (const [assetId] of batch) { + this.inFlight.delete(assetId); + } + } } } diff --git a/src/lib/services/rank_batcher.ts b/src/lib/services/rank_batcher.ts index 3f405198..f10fc9ad 100644 --- a/src/lib/services/rank_batcher.ts +++ b/src/lib/services/rank_batcher.ts @@ -2,7 +2,7 @@ import {environment} from '../../environment'; import {DeferredPromise} from '../utils/deferred_promise'; const RANKS_CHECK_URL = `${environment.floatdb_gateway_url}/v1/ranks/check`; -const DEBOUNCE_MS = 50; +const DEBOUNCE_MS = 100; export interface RankResult { low_rank?: number; @@ -16,12 +16,12 @@ interface PendingItem { /** * Batches concurrent rank check requests into a single POST to /v1/ranks/check. - * Callers receive individual promises; a short debounce window (50ms) collects + * Callers receive individual promises; a short debounce window collects * qualifying items so that bulk page loads produce one network request instead of many. */ class RankBatcher { private pending = new Map(); - private timer: ReturnType | null = null; + private debounceTimer: ReturnType | null = null; check(link: string, assetId: string): Promise { const existing = this.pending.get(assetId); @@ -32,16 +32,19 @@ class RankBatcher { const deferred = new DeferredPromise(); this.pending.set(assetId, {link, deferred}); - if (this.timer !== null) { - clearTimeout(this.timer); + if (this.debounceTimer !== null) { + clearTimeout(this.debounceTimer); } - this.timer = setTimeout(() => this.flush(), DEBOUNCE_MS); + this.debounceTimer = setTimeout(() => this.flush(), DEBOUNCE_MS); return deferred.promise(); } private async flush(): Promise { - this.timer = null; + if (this.debounceTimer !== null) { + clearTimeout(this.debounceTimer); + this.debounceTimer = null; + } const batch = new Map(this.pending); this.pending.clear(); diff --git a/src/lib/types/steam.d.ts b/src/lib/types/steam.d.ts index 1aa7defc..09457c2a 100644 --- a/src/lib/types/steam.d.ts +++ b/src/lib/types/steam.d.ts @@ -84,7 +84,7 @@ interface rgAssetPropertyBase { } // Only one of int_value, float_value, or string_value can be present -type rgAssetProperty = RequireOnlyOne; +export type rgAssetProperty = RequireOnlyOne; // g_rgAssets export interface rgAsset extends rgDescription { From afc436d172dccbb5ecb33095cd18f74047bfbbb8 Mon Sep 17 00:00:00 2001 From: Step7750 Date: Wed, 8 Apr 2026 23:49:47 -0600 Subject: [PATCH 03/12] enables annotating inventory cards that are hidden there is no meaningful downside since pages are added incrementally as you paginate --- src/lib/components/inventory/inventory_item_holder_metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/inventory/inventory_item_holder_metadata.ts b/src/lib/components/inventory/inventory_item_holder_metadata.ts index a9d672a4..0b654405 100644 --- a/src/lib/components/inventory/inventory_item_holder_metadata.ts +++ b/src/lib/components/inventory/inventory_item_holder_metadata.ts @@ -6,7 +6,7 @@ import {isCAppwideInventory} from '../../utils/checkers'; @CustomElement() @InjectAppend( - '#active_inventory_page div.inventory_page:not([style*="display: none"]) .itemHolder div.app730', + '#active_inventory_page div.inventory_page .itemHolder div.app730', InjectionMode.CONTINUOUS ) export class InventoryItemHolderMetadata extends ItemHolderMetadata { From eba6b5205a954f24ac449baeba79e49af961110b Mon Sep 17 00:00:00 2001 From: Step7750 Date: Wed, 8 Apr 2026 23:55:44 -0600 Subject: [PATCH 04/12] refactor inventory observe --- src/lib/page_scripts/inventory.ts | 42 +++++++++++-------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/lib/page_scripts/inventory.ts b/src/lib/page_scripts/inventory.ts index 4df78bf0..2226a517 100644 --- a/src/lib/page_scripts/inventory.ts +++ b/src/lib/page_scripts/inventory.ts @@ -20,34 +20,25 @@ async function main() { * Instead, we eagerly fetch the ranks for all items that have been loaded. */ Observe(() => { - const allProps = getAllCS2AssetProperties(); - const propsLength = Object.keys(allProps).length; - if (propsLength > 0 && !initialRun) { - // Ensure that we run the function at least once if we happened to load - // and all the properties were already there. + const count = Object.keys(getAllCS2AssetProperties()).length; + if (count > 0 && !initialRun) { initialRun = true; return true; } - return Object.keys(allProps).length; + return count; }, () => { if (typeof g_ActiveInventory === 'undefined') return; - const assetProperties = getAllCS2AssetProperties(); + for (const [asset_id, props] of Object.entries(getAllCS2AssetProperties())) { + // No float value, skip + if (!props.some(e => e.propertyid === 2)) continue; - for (const [asset_id, props] of Object.entries(assetProperties)) { - if (!props.find(e => e.propertyid === 2)) { - // doesn't have a float, skip - continue; - } - - const serializedLink = props.find(e => e.propertyid === 6); - if (!serializedLink?.string_value) { - continue; - } + const inspectLink = props.find(e => e.propertyid === 6)?.string_value; + if (!inspectLink) continue; gFloatFetcher.fetch({ - asset_id: asset_id, - link: `steam://run/730//+csgo_econ_action_preview%20${serializedLink.string_value}` + asset_id, + link: `steam://run/730//+csgo_econ_action_preview%20${inspectLink}`, }); } }) @@ -59,14 +50,11 @@ function getAllCS2AssetProperties(): {[assetId: string]: rgAssetProperty[]} { const allProperties = g_ActiveInventory.m_rgAssetProperties || {}; if (isCAppwideInventory(g_ActiveInventory)) { - const primaryProps = g_ActiveInventory.m_rgChildInventories[ContextId.PRIMARY]?.m_rgAssetProperties; - if (Object.keys(primaryProps).length > 0) { - Object.assign(allProperties, primaryProps); - } - - const protectedProps = g_ActiveInventory.m_rgChildInventories[ContextId.PROTECTED]?.m_rgAssetProperties; - if (Object.keys(protectedProps).length > 0) { - Object.assign(allProperties, protectedProps); + for (const contextId of [ContextId.PRIMARY, ContextId.PROTECTED]) { + const props = g_ActiveInventory.m_rgChildInventories[contextId]?.m_rgAssetProperties; + if (props && Object.keys(props).length > 0) { + Object.assign(allProperties, props); + } } } From 1ab5bad025c89b3e497b77ecfbf39791e5fccf4e Mon Sep 17 00:00:00 2001 From: Step7750 Date: Thu, 9 Apr 2026 00:06:25 -0600 Subject: [PATCH 05/12] removes unused `listPrice` --- src/lib/bridge/handlers/fetch_inspect_info.ts | 1 - src/lib/components/market/item_row_wrapper.ts | 9 --------- 2 files changed, 10 deletions(-) diff --git a/src/lib/bridge/handlers/fetch_inspect_info.ts b/src/lib/bridge/handlers/fetch_inspect_info.ts index f985efcb..bbbb8809 100644 --- a/src/lib/bridge/handlers/fetch_inspect_info.ts +++ b/src/lib/bridge/handlers/fetch_inspect_info.ts @@ -45,7 +45,6 @@ export interface ItemInfo { export interface FetchInspectInfoRequest { link: string; asset_id: string; - listPrice?: number; } export interface FetchInspectInfoResponse { diff --git a/src/lib/components/market/item_row_wrapper.ts b/src/lib/components/market/item_row_wrapper.ts index 35983135..bb143003 100644 --- a/src/lib/components/market/item_row_wrapper.ts +++ b/src/lib/components/market/item_row_wrapper.ts @@ -86,7 +86,6 @@ export class ItemRowWrapper extends FloatElement { return gFloatFetcher.fetch({ link: this.inspectLink!, asset_id: this.listingInfo?.asset.id!, - listPrice: this.usdPrice, }); } @@ -112,14 +111,6 @@ export class ItemRowWrapper extends FloatElement { return (this.listingInfo.converted_price + this.listingInfo.converted_fee) / 100; } - get usdPrice(): number | undefined { - if (this.listingInfo?.currencyid === Currency.USD) { - return this.listingInfo.price + this.listingInfo.fee; - } else if (this.listingInfo?.converted_currencyid === Currency.USD) { - return this.listingInfo.converted_price! + this.listingInfo.converted_fee!; - } - } - @state() private itemInfo: ItemInfo | undefined; @state() From c2724fad3fe46051124ae33c43cbbb4284bfc06f Mon Sep 17 00:00:00 2001 From: Step7750 Date: Thu, 9 Apr 2026 00:30:01 -0600 Subject: [PATCH 06/12] caches thresholds in localstorage --- src/lib/services/threshold_fetcher.ts | 62 ++++++++++++++++++++++++--- src/lib/storage/keys.ts | 5 +++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/lib/services/threshold_fetcher.ts b/src/lib/services/threshold_fetcher.ts index 42d94942..5a2fd5be 100644 --- a/src/lib/services/threshold_fetcher.ts +++ b/src/lib/services/threshold_fetcher.ts @@ -1,13 +1,21 @@ import {environment} from '../../environment'; +import {gStore} from '../storage/store'; +import {THRESHOLD_CACHE} from '../storage/keys'; const THRESHOLDS_URL = `${environment.floatdb_gateway_url}/v1/ranks/thresholds/bin`; const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour +const RETRY_AFTER_FAILURE_MS = 15 * 60 * 1000; // 15 minutes interface ThresholdEntry { low: number; high: number; } +interface ThresholdCache { + lastUpdated: number; + thresholds: Record; +} + function makeKey(defindex: number, paintindex: number, stattrak: boolean, souvenir: boolean): string { return `${defindex}:${paintindex}:${stattrak ? 1 : 0}:${souvenir ? 1 : 0}`; } @@ -44,7 +52,8 @@ function parseThresholdsBinary(buffer: ArrayBuffer): Map class ThresholdFetcher { private thresholds: Map | null = null; private lastFetched = 0; - private pendingFetch?: Promise>; + private lastFailedAt = 0; + private pendingFetch?: Promise | null>; async qualifiesForRankCheck( defindex: number, @@ -54,12 +63,14 @@ class ThresholdFetcher { floatvalue: number ): Promise { const thresholds = await this.getThresholds(); + if (!thresholds) return false; + const entry = thresholds.get(makeKey(defindex, paintindex, stattrak, souvenir)); if (!entry) return false; return floatvalue <= entry.low || floatvalue >= entry.high; } - private async getThresholds(): Promise> { + private async getThresholds(): Promise | null> { if (this.thresholds && Date.now() - this.lastFetched < CACHE_DURATION_MS) { return this.thresholds; } @@ -68,7 +79,11 @@ class ThresholdFetcher { return this.pendingFetch; } - const fetchPromise = this.fetchThresholds(); + if (this.lastFailedAt && Date.now() - this.lastFailedAt < RETRY_AFTER_FAILURE_MS) { + return this.thresholds; + } + + const fetchPromise = this.resolveThresholds(); this.pendingFetch = fetchPromise; try { @@ -80,7 +95,24 @@ class ThresholdFetcher { } } - private async fetchThresholds(): Promise> { + private async resolveThresholds(): Promise | null> { + const storedCache = await gStore.getWithStorage( + chrome.storage.local, + THRESHOLD_CACHE.key + ); + + if (storedCache?.thresholds && Date.now() - storedCache.lastUpdated < CACHE_DURATION_MS) { + this.thresholds = new Map(Object.entries(storedCache.thresholds)); + this.lastFetched = storedCache.lastUpdated; + return this.thresholds; + } + + return this.fetchThresholds(storedCache); + } + + private async fetchThresholds( + storedCache: ThresholdCache | null + ): Promise | null> { try { const resp = await fetch(THRESHOLDS_URL); if (!resp.ok) { @@ -92,14 +124,30 @@ class ThresholdFetcher { this.thresholds = parsed; this.lastFetched = Date.now(); + this.lastFailedAt = 0; + + const serializable = Object.fromEntries(parsed); + await gStore.setWithStorage(chrome.storage.local, THRESHOLD_CACHE.key, { + lastUpdated: this.lastFetched, + thresholds: serializable, + }); return parsed; } catch (error) { - if (this.thresholds) { - console.error('Error fetching thresholds, using cached:', error); + console.error('Error fetching thresholds:', error); + this.lastFailedAt = Date.now(); + + // Fallback to last successful threshold fetch + if (this.thresholds) return this.thresholds; + + // Fallback to last successful stored fetch + if (storedCache?.thresholds) { + this.thresholds = new Map(Object.entries(storedCache.thresholds)); + this.lastFetched = storedCache.lastUpdated; return this.thresholds; } - throw error; + + return null; } } } diff --git a/src/lib/storage/keys.ts b/src/lib/storage/keys.ts index 5035e44a..d5c2d292 100644 --- a/src/lib/storage/keys.ts +++ b/src/lib/storage/keys.ts @@ -14,6 +14,7 @@ export enum StorageKey { LAST_TRADE_BLOCKED_PING_ATTEMPT = 'last_trade_blocked_ping_attempt', PRICE_CACHE = 'price_cache', // Stores market hash name -> price mapping (~0.86MB) SCHEMA_CACHE = 'schema_cache', // Stores the full CSFloat schema payload + THRESHOLD_CACHE = 'threshold_cache', // Stores FloatDB rank thresholds } export type DynamicStorageKey = string; @@ -59,3 +60,7 @@ export const SCHEMA_CACHE = newRow<{ lastUpdated: number; schema: ItemSchema.Response; }>(StorageKey.SCHEMA_CACHE); +export const THRESHOLD_CACHE = newRow<{ + lastUpdated: number; + thresholds: Record; +}>(StorageKey.THRESHOLD_CACHE); From 2d67f63acf0ef4057275cb425ba86a84fca16ca9 Mon Sep 17 00:00:00 2001 From: Step7750 Date: Mon, 13 Apr 2026 00:28:04 -0600 Subject: [PATCH 07/12] fix lint --- src/lib/components/market/item_row_wrapper.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/components/market/item_row_wrapper.ts b/src/lib/components/market/item_row_wrapper.ts index bb143003..da8f6bef 100644 --- a/src/lib/components/market/item_row_wrapper.ts +++ b/src/lib/components/market/item_row_wrapper.ts @@ -83,9 +83,13 @@ export class ItemRowWrapper extends FloatElement { } async fetchFloat(): Promise { + if (!this.listingInfo?.asset.id) { + throw new Error('Missing asset ID'); + } + return gFloatFetcher.fetch({ link: this.inspectLink!, - asset_id: this.listingInfo?.asset.id!, + asset_id: this.listingInfo?.asset.id, }); } From 45866afa0aabd77c1e2bb78f8923ebe876d803cf Mon Sep 17 00:00:00 2001 From: Step7750 Date: Mon, 13 Apr 2026 00:29:20 -0600 Subject: [PATCH 08/12] prettier --- src/lib/bridge/handlers/fetch_inspect_info.ts | 5 +-- .../inventory_item_holder_metadata.ts | 5 +-- src/lib/page_scripts/inventory.ts | 41 ++++++++++--------- src/lib/services/threshold_fetcher.ts | 9 +--- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/lib/bridge/handlers/fetch_inspect_info.ts b/src/lib/bridge/handlers/fetch_inspect_info.ts index bbbb8809..5064e3e0 100644 --- a/src/lib/bridge/handlers/fetch_inspect_info.ts +++ b/src/lib/bridge/handlers/fetch_inspect_info.ts @@ -52,10 +52,7 @@ export interface FetchInspectInfoResponse { error?: string; } -async function processInspectItem( - req: FetchInspectInfoRequest, - schema: ItemSchema.Response -): Promise { +async function processInspectItem(req: FetchInspectInfoRequest, schema: ItemSchema.Response): Promise { let decoded: CEconItemPreviewDataBlock; try { decoded = decodeLink(req.link); diff --git a/src/lib/components/inventory/inventory_item_holder_metadata.ts b/src/lib/components/inventory/inventory_item_holder_metadata.ts index 0b654405..1b57406d 100644 --- a/src/lib/components/inventory/inventory_item_holder_metadata.ts +++ b/src/lib/components/inventory/inventory_item_holder_metadata.ts @@ -5,10 +5,7 @@ import {ContextId} from '../../types/steam_constants'; import {isCAppwideInventory} from '../../utils/checkers'; @CustomElement() -@InjectAppend( - '#active_inventory_page div.inventory_page .itemHolder div.app730', - InjectionMode.CONTINUOUS -) +@InjectAppend('#active_inventory_page div.inventory_page .itemHolder div.app730', InjectionMode.CONTINUOUS) export class InventoryItemHolderMetadata extends ItemHolderMetadata { get asset(): rgAsset | undefined { if (!this.assetId) return; diff --git a/src/lib/page_scripts/inventory.ts b/src/lib/page_scripts/inventory.ts index 2226a517..a63f0a1c 100644 --- a/src/lib/page_scripts/inventory.ts +++ b/src/lib/page_scripts/inventory.ts @@ -19,29 +19,32 @@ async function main() { * * Instead, we eagerly fetch the ranks for all items that have been loaded. */ - Observe(() => { - const count = Object.keys(getAllCS2AssetProperties()).length; - if (count > 0 && !initialRun) { - initialRun = true; - return true; - } - return count; - }, () => { - if (typeof g_ActiveInventory === 'undefined') return; + Observe( + () => { + const count = Object.keys(getAllCS2AssetProperties()).length; + if (count > 0 && !initialRun) { + initialRun = true; + return true; + } + return count; + }, + () => { + if (typeof g_ActiveInventory === 'undefined') return; - for (const [asset_id, props] of Object.entries(getAllCS2AssetProperties())) { - // No float value, skip - if (!props.some(e => e.propertyid === 2)) continue; + for (const [asset_id, props] of Object.entries(getAllCS2AssetProperties())) { + // No float value, skip + if (!props.some((e) => e.propertyid === 2)) continue; - const inspectLink = props.find(e => e.propertyid === 6)?.string_value; - if (!inspectLink) continue; + const inspectLink = props.find((e) => e.propertyid === 6)?.string_value; + if (!inspectLink) continue; - gFloatFetcher.fetch({ - asset_id, - link: `steam://run/730//+csgo_econ_action_preview%20${inspectLink}`, - }); + gFloatFetcher.fetch({ + asset_id, + link: `steam://run/730//+csgo_econ_action_preview%20${inspectLink}`, + }); + } } - }) + ); } function getAllCS2AssetProperties(): {[assetId: string]: rgAssetProperty[]} { diff --git a/src/lib/services/threshold_fetcher.ts b/src/lib/services/threshold_fetcher.ts index 5a2fd5be..3ab82089 100644 --- a/src/lib/services/threshold_fetcher.ts +++ b/src/lib/services/threshold_fetcher.ts @@ -96,10 +96,7 @@ class ThresholdFetcher { } private async resolveThresholds(): Promise | null> { - const storedCache = await gStore.getWithStorage( - chrome.storage.local, - THRESHOLD_CACHE.key - ); + const storedCache = await gStore.getWithStorage(chrome.storage.local, THRESHOLD_CACHE.key); if (storedCache?.thresholds && Date.now() - storedCache.lastUpdated < CACHE_DURATION_MS) { this.thresholds = new Map(Object.entries(storedCache.thresholds)); @@ -110,9 +107,7 @@ class ThresholdFetcher { return this.fetchThresholds(storedCache); } - private async fetchThresholds( - storedCache: ThresholdCache | null - ): Promise | null> { + private async fetchThresholds(storedCache: ThresholdCache | null): Promise | null> { try { const resp = await fetch(THRESHOLDS_URL); if (!resp.ok) { From d30bfd0013be636f657d6165eefab400444b39de Mon Sep 17 00:00:00 2001 From: Step7750 Date: Mon, 13 Apr 2026 00:40:13 -0600 Subject: [PATCH 09/12] creates shallow copy for props --- src/lib/page_scripts/inventory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/page_scripts/inventory.ts b/src/lib/page_scripts/inventory.ts index a63f0a1c..4102d9a3 100644 --- a/src/lib/page_scripts/inventory.ts +++ b/src/lib/page_scripts/inventory.ts @@ -50,7 +50,7 @@ async function main() { function getAllCS2AssetProperties(): {[assetId: string]: rgAssetProperty[]} { if (typeof g_ActiveInventory === 'undefined') return {}; - const allProperties = g_ActiveInventory.m_rgAssetProperties || {}; + const allProperties = Object.assign({}, g_ActiveInventory.m_rgAssetProperties || {}); if (isCAppwideInventory(g_ActiveInventory)) { for (const contextId of [ContextId.PRIMARY, ContextId.PROTECTED]) { From 616b96c341a8b596067e71aaed26869fb2fb532b Mon Sep 17 00:00:00 2001 From: Step7750 Date: Mon, 13 Apr 2026 00:41:54 -0600 Subject: [PATCH 10/12] catch float fetcher error --- src/lib/page_scripts/inventory.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib/page_scripts/inventory.ts b/src/lib/page_scripts/inventory.ts index 4102d9a3..6cf37013 100644 --- a/src/lib/page_scripts/inventory.ts +++ b/src/lib/page_scripts/inventory.ts @@ -38,10 +38,14 @@ async function main() { const inspectLink = props.find((e) => e.propertyid === 6)?.string_value; if (!inspectLink) continue; - gFloatFetcher.fetch({ - asset_id, - link: `steam://run/730//+csgo_econ_action_preview%20${inspectLink}`, - }); + gFloatFetcher + .fetch({ + asset_id, + link: `steam://run/730//+csgo_econ_action_preview%20${inspectLink}`, + }) + .catch((e) => { + console.error(`Failed to eagerly fetch ${inspectLink}: ${e}`); + }); } } ); From d5562e7f58db72ea054f508bcb534611f6f7f8b0 Mon Sep 17 00:00:00 2001 From: Step7750 Date: Wed, 15 Apr 2026 00:24:51 -0600 Subject: [PATCH 11/12] address comments --- src/lib/bridge/handlers/fetch_inspect_info.ts | 2 +- src/lib/services/threshold_fetcher.ts | 2 +- src/lib/storage/keys.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/bridge/handlers/fetch_inspect_info.ts b/src/lib/bridge/handlers/fetch_inspect_info.ts index 5064e3e0..748298a8 100644 --- a/src/lib/bridge/handlers/fetch_inspect_info.ts +++ b/src/lib/bridge/handlers/fetch_inspect_info.ts @@ -132,7 +132,7 @@ async function processInspectItem(req: FetchInspectInfoRequest, schema: ItemSche try { if (decoded.itemid != null && decoded.paintwear != null) { const stattrak = decoded.killeaterscoretype !== undefined; - const souvenir = (decoded.quality ?? 0) === 12; + const souvenir = decoded.quality === 12; if (await gThresholdFetcher.qualifiesForRankCheck(defindex, paintindex, stattrak, souvenir, floatvalue)) { const rankResult = await gRankBatcher.check(req.link, decoded.itemid.toString()); diff --git a/src/lib/services/threshold_fetcher.ts b/src/lib/services/threshold_fetcher.ts index 3ab82089..bf9de426 100644 --- a/src/lib/services/threshold_fetcher.ts +++ b/src/lib/services/threshold_fetcher.ts @@ -6,7 +6,7 @@ const THRESHOLDS_URL = `${environment.floatdb_gateway_url}/v1/ranks/thresholds/b const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour const RETRY_AFTER_FAILURE_MS = 15 * 60 * 1000; // 15 minutes -interface ThresholdEntry { +export interface ThresholdEntry { low: number; high: number; } diff --git a/src/lib/storage/keys.ts b/src/lib/storage/keys.ts index d5c2d292..07d39279 100644 --- a/src/lib/storage/keys.ts +++ b/src/lib/storage/keys.ts @@ -3,6 +3,7 @@ */ import {SerializedFilter} from '../filter/types'; import type {ItemSchema} from '../types/schema'; +import type {ThresholdEntry} from '../services/threshold_fetcher'; export enum StorageKey { // Backwards compatible with <3.0.0 @@ -62,5 +63,5 @@ export const SCHEMA_CACHE = newRow<{ }>(StorageKey.SCHEMA_CACHE); export const THRESHOLD_CACHE = newRow<{ lastUpdated: number; - thresholds: Record; + thresholds: Record; }>(StorageKey.THRESHOLD_CACHE); From 8204d10022298ff2a298ce862d54e867cddb0b63 Mon Sep 17 00:00:00 2001 From: Step7750 Date: Wed, 15 Apr 2026 00:38:48 -0600 Subject: [PATCH 12/12] fixes cycle --- src/lib/services/threshold_fetcher.ts | 6 +----- src/lib/storage/keys.ts | 2 +- src/lib/types/floatdb.ts | 4 ++++ 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 src/lib/types/floatdb.ts diff --git a/src/lib/services/threshold_fetcher.ts b/src/lib/services/threshold_fetcher.ts index bf9de426..faab65bc 100644 --- a/src/lib/services/threshold_fetcher.ts +++ b/src/lib/services/threshold_fetcher.ts @@ -1,16 +1,12 @@ import {environment} from '../../environment'; import {gStore} from '../storage/store'; import {THRESHOLD_CACHE} from '../storage/keys'; +import type {ThresholdEntry} from '../types/floatdb'; const THRESHOLDS_URL = `${environment.floatdb_gateway_url}/v1/ranks/thresholds/bin`; const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour const RETRY_AFTER_FAILURE_MS = 15 * 60 * 1000; // 15 minutes -export interface ThresholdEntry { - low: number; - high: number; -} - interface ThresholdCache { lastUpdated: number; thresholds: Record; diff --git a/src/lib/storage/keys.ts b/src/lib/storage/keys.ts index 07d39279..28caa248 100644 --- a/src/lib/storage/keys.ts +++ b/src/lib/storage/keys.ts @@ -3,7 +3,7 @@ */ import {SerializedFilter} from '../filter/types'; import type {ItemSchema} from '../types/schema'; -import type {ThresholdEntry} from '../services/threshold_fetcher'; +import type {ThresholdEntry} from '../types/floatdb'; export enum StorageKey { // Backwards compatible with <3.0.0 diff --git a/src/lib/types/floatdb.ts b/src/lib/types/floatdb.ts new file mode 100644 index 00000000..c29daf8d --- /dev/null +++ b/src/lib/types/floatdb.ts @@ -0,0 +1,4 @@ +export interface ThresholdEntry { + low: number; + high: number; +}