fix: IREN/ASTS/IRE stuck-pending + corridor feature

Fix stuck adapter_queue jobs (ASTS/IRE/IREN stuck pending forever):

1. fetchSpec early-return paths (cooldown checks) now update job status
   to 'backoff' with last_attempt set and 30s backoff_until, instead of
   returning without any status change. Prevents jobs from being
   re-processed every drain cycle indefinitely.

2. Wrap adapter.fetchOne() in 30s Promise.race timeout. A hung HTTP
   request no longer blocks the entire per-source promise chain
   forever, preventing all subsequent jobs for that source.

Also includes corridor/confluence feature, tiered quote schedules,
cache improvements, and HoldingsBookView refinements.
This commit is contained in:
Investor Flow Build
2026-08-11 22:25:17 -04:00
parent 24349a8b6d
commit 9b41890d33
28 changed files with 2576 additions and 184 deletions
+22
View File
@@ -8,6 +8,7 @@ import { db as defaultDb } from '../db/client.ts';
import {
CANDLE_FRESH_MS,
quoteTtlMs,
tieredQuoteTtlMs,
SYMBOL_META_INCOMPLETE_TTL_MS,
} from '../queue/sourceRatePolicy.ts';
import { KvReadCache } from './LruCache.ts';
@@ -190,6 +191,13 @@ export function needsQuoteRefresh(d: DatabaseSync, symbol: string, now = Date.no
return tsAgeMs(row.observed_at, now) > quoteTtlMs(new Date(now));
}
/** Tier-aware quote freshness check. Portfolio (T0) gets the tightest TTL. */
export function needsTieredQuoteRefresh(d: DatabaseSync, symbol: string, tier: number, now = Date.now()): boolean {
const row = d.prepare('SELECT observed_at FROM quotes WHERE symbol=?').get(symbol) as { observed_at: string } | undefined;
if (!row?.observed_at) return true;
return tsAgeMs(row.observed_at, now) > tieredQuoteTtlMs(tier, new Date(now));
}
/** True when symbol meta missing, incomplete (no name), or past weekly TTL. */
export function needsSymbolMetaRefresh(d: DatabaseSync, symbol: string, now = Date.now()): boolean {
const row = d.prepare('SELECT name, sector, updated_at FROM symbols WHERE symbol=?').get(symbol) as
@@ -617,6 +625,8 @@ export interface CacheRepository {
* Safe to call on every Market Outlook / ticker context load.
*/
ensureInDemand(symbol: string, tickerKind: TickerKind): Promise<void>;
/** Bump symbol to watched tier (2) on page view. Decays back after 10 min. */
bumpToWatched(symbol: string, tickerKind: TickerKind): Promise<void>;
/** Permanent system pin (rotation universe, SPY, VIX) — survives unsubscribe. */
pinSystemSymbol(symbol: string, tickerKind: TickerKind): Promise<void>;
demandSet(): Promise<string[]>;
@@ -735,6 +745,18 @@ export class CacheRepositoryImpl implements CacheRepository {
await this.queueIfNeeded(symbol);
}
/** Bump a symbol to watched tier (2) on page view. The periodic tier
* recompute decays it back to background after ~10 min of inactivity. */
async bumpToWatched(symbol: string, tickerKind: TickerKind): Promise<void> {
this.ensureDemandRow(symbol, tickerKind);
const now = new Date().toISOString();
// Only lower tier (raise priority) — never raise tier above current.
this._db.prepare(
"UPDATE symbol_demand SET tier = MIN(tier, 2), last_viewed_at = ?, in_demand = 1 WHERE symbol=?",
).run(now, symbol);
await this.queueIfNeeded(symbol);
}
async pinSystemSymbol(symbol: string, tickerKind: TickerKind): Promise<void> {
this.ensureDemandRow(symbol, tickerKind);
this._db.prepare(