mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(sidecar): upstream concurrency limiter, Yahoo rate gate, startup batching (#1145)
- Sidecar: add global concurrency limiter (max 6 concurrent upstream requests) - Sidecar: add Yahoo Finance rate gate (600ms spacing) in fetch patch - Sidecar: fix default remoteBase to api.worldmonitor.app - data-loader: stagger startup tasks in batches of 4 with 300ms delay - get-country-stock-index: add yahooGate() before Yahoo fetch - tauri.conf: add titleBarStyle Overlay
This commit is contained in:
@@ -59,43 +59,80 @@ function isTransientVerificationError(error) {
|
||||
return /timed out|timeout|network|fetch failed|failed to fetch|socket hang up/i.test(error.message);
|
||||
}
|
||||
|
||||
// Global concurrency limiter for upstream requests.
|
||||
let _activeUpstream = 0;
|
||||
const _upstreamQueue = [];
|
||||
const MAX_CONCURRENT_UPSTREAM = 6;
|
||||
function acquireUpstreamSlot() {
|
||||
if (_activeUpstream < MAX_CONCURRENT_UPSTREAM) {
|
||||
_activeUpstream++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => _upstreamQueue.push(resolve));
|
||||
}
|
||||
function releaseUpstreamSlot() {
|
||||
if (_upstreamQueue.length > 0) {
|
||||
_upstreamQueue.shift()();
|
||||
} else {
|
||||
_activeUpstream--;
|
||||
}
|
||||
}
|
||||
|
||||
// Global Yahoo Finance rate gate — shared across ALL handler bundles.
|
||||
let _yahooLastReq = 0;
|
||||
let _yahooQueue = Promise.resolve();
|
||||
function sidecarYahooGate() {
|
||||
_yahooQueue = _yahooQueue.then(async () => {
|
||||
const elapsed = Date.now() - _yahooLastReq;
|
||||
if (elapsed < 600) await new Promise(r => setTimeout(r, 600 - elapsed));
|
||||
_yahooLastReq = Date.now();
|
||||
});
|
||||
return _yahooQueue;
|
||||
}
|
||||
|
||||
globalThis.fetch = async function ipv4Fetch(input, init) {
|
||||
const isRequest = input && typeof input === 'object' && 'url' in input;
|
||||
let url;
|
||||
try { url = new URL(typeof input === 'string' ? input : input.url); } catch { return _originalFetch(input, init); }
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') return _originalFetch(input, init);
|
||||
const mod = url.protocol === 'https:' ? https : http;
|
||||
const method = init?.method || (isRequest ? input.method : 'GET');
|
||||
const body = await resolveRequestBody(input, init, method, isRequest);
|
||||
const headers = {};
|
||||
const rawHeaders = init?.headers || (isRequest ? input.headers : null);
|
||||
if (rawHeaders) {
|
||||
const h = rawHeaders instanceof Headers ? Object.fromEntries(rawHeaders.entries())
|
||||
: Array.isArray(rawHeaders) ? Object.fromEntries(rawHeaders) : rawHeaders;
|
||||
Object.assign(headers, h);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = mod.request({ hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method, headers, family: 4 }, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (c) => chunks.push(c));
|
||||
res.on('end', () => {
|
||||
const buf = Buffer.concat(chunks);
|
||||
const responseHeaders = new Headers();
|
||||
for (const [k, v] of Object.entries(res.headers)) {
|
||||
if (v) responseHeaders.set(k, Array.isArray(v) ? v.join(', ') : v);
|
||||
}
|
||||
try {
|
||||
resolve(buildSafeResponse(res.statusCode, res.statusMessage, responseHeaders, buf));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
if (url.hostname.includes('finance.yahoo.com')) await sidecarYahooGate();
|
||||
await acquireUpstreamSlot();
|
||||
try {
|
||||
const mod = url.protocol === 'https:' ? https : http;
|
||||
const method = init?.method || (isRequest ? input.method : 'GET');
|
||||
const body = await resolveRequestBody(input, init, method, isRequest);
|
||||
const headers = {};
|
||||
const rawHeaders = init?.headers || (isRequest ? input.headers : null);
|
||||
if (rawHeaders) {
|
||||
const h = rawHeaders instanceof Headers ? Object.fromEntries(rawHeaders.entries())
|
||||
: Array.isArray(rawHeaders) ? Object.fromEntries(rawHeaders) : rawHeaders;
|
||||
Object.assign(headers, h);
|
||||
}
|
||||
return await new Promise((resolve, reject) => {
|
||||
const req = mod.request({ hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method, headers, family: 4 }, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (c) => chunks.push(c));
|
||||
res.on('end', () => {
|
||||
const buf = Buffer.concat(chunks);
|
||||
const responseHeaders = new Headers();
|
||||
for (const [k, v] of Object.entries(res.headers)) {
|
||||
if (v) responseHeaders.set(k, Array.isArray(v) ? v.join(', ') : v);
|
||||
}
|
||||
try {
|
||||
resolve(buildSafeResponse(res.statusCode, res.statusMessage, responseHeaders, buf));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (init?.signal) { init.signal.addEventListener('abort', () => req.destroy()); }
|
||||
if (body != null) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (init?.signal) { init.signal.addEventListener('abort', () => req.destroy()); }
|
||||
if (body != null) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
} finally {
|
||||
releaseUpstreamSlot();
|
||||
}
|
||||
};
|
||||
|
||||
const ALLOWED_ENV_KEYS = new Set([
|
||||
@@ -452,7 +489,7 @@ async function importHandler(modulePath) {
|
||||
|
||||
function resolveConfig(options = {}) {
|
||||
const port = Number(options.port ?? process.env.LOCAL_API_PORT ?? 46123);
|
||||
const remoteBase = String(options.remoteBase ?? process.env.LOCAL_API_REMOTE_BASE ?? 'https://worldmonitor.app').replace(/\/$/, '');
|
||||
const remoteBase = String(options.remoteBase ?? process.env.LOCAL_API_REMOTE_BASE ?? 'https://api.worldmonitor.app').replace(/\/$/, '');
|
||||
const resourceDir = String(options.resourceDir ?? process.env.LOCAL_API_RESOURCE_DIR ?? process.cwd());
|
||||
const apiDir = options.apiDir
|
||||
? String(options.apiDir)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "World Monitor",
|
||||
"titleBarStyle": "Overlay",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"minWidth": 1200,
|
||||
|
||||
Reference in New Issue
Block a user