feat(interceptor): XHR/fetch wrap to detect Train responses
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Wraps XHR and fetch to listen for the request Torn sends when the
|
||||
* user clicks "Train". When such a request is detected, the response
|
||||
* is parsed via `parseTrainResponse`, the new attribute value is
|
||||
* compared against the previous one, and `onTrain({attr, delta, ts})`
|
||||
* is invoked.
|
||||
*
|
||||
* `parseTrainResponse(text, url)` is intentionally permissive and
|
||||
* returns `{ newValue, attr } | null`. The default implementation
|
||||
* tries JSON first, then a regex fallback.
|
||||
*/
|
||||
export function startRequestInterceptor({ prevValue, currentAttr, onTrain, onParseFail }) {
|
||||
let lastValue = prevValue;
|
||||
let warnedFor = null;
|
||||
|
||||
const handle = (text, url) => {
|
||||
const parsed = parseTrainResponse(text, url);
|
||||
if (!parsed) {
|
||||
if (warnedFor !== url) {
|
||||
warnedFor = url;
|
||||
onParseFail && onParseFail(url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsed.attr !== currentAttr) return;
|
||||
const delta = parsed.newValue - lastValue;
|
||||
lastValue = parsed.newValue;
|
||||
if (delta > 0) onTrain({ attr: parsed.attr, delta, ts: Date.now() });
|
||||
};
|
||||
|
||||
wrapXhr(handle);
|
||||
wrapFetch(handle);
|
||||
|
||||
return {
|
||||
updatePrevValue(v) { lastValue = v; },
|
||||
};
|
||||
}
|
||||
|
||||
function parseTrainResponse(text, url) {
|
||||
// Try JSON
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
// Torn historically returns an HTML fragment; if it's JSON, look
|
||||
// for a known shape. This is a placeholder — adjust after manual
|
||||
// verification.
|
||||
if (j && typeof j === 'object' && 'newValue' in j && 'attr' in j) {
|
||||
return { newValue: Number(j.newValue), attr: String(j.attr) };
|
||||
}
|
||||
} catch { /* not JSON */ }
|
||||
|
||||
// Fallback: scan text for a number formatted like an attribute.
|
||||
const m = text.match(/(\d{1,3}(?:,\d{3})+|\d{4,})/);
|
||||
if (m) {
|
||||
const newValue = parseInt(m[1].replace(/,/g, ''), 10);
|
||||
if (Number.isFinite(newValue) && newValue > 0) {
|
||||
// Without a confirmed attr, fall back to whatever currentAttr
|
||||
// the caller passed in.
|
||||
return { newValue, attr: null };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function wrapXhr(handle) {
|
||||
const origOpen = XMLHttpRequest.prototype.open;
|
||||
const origSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
this.__tatUrl = String(url);
|
||||
return origOpen.apply(this, arguments);
|
||||
};
|
||||
XMLHttpRequest.prototype.send = function () {
|
||||
this.addEventListener('load', () => {
|
||||
try { handle(this.responseText, this.__tatUrl); } catch { /* noop */ }
|
||||
});
|
||||
return origSend.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
function wrapFetch(handle) {
|
||||
const origFetch = window.fetch;
|
||||
if (origFetch.__tatWrapped) return;
|
||||
window.fetch = async function (...args) {
|
||||
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
|
||||
const res = await origFetch.apply(this, args);
|
||||
try {
|
||||
const text = await res.clone().text();
|
||||
handle(text, String(url));
|
||||
} catch { /* noop */ }
|
||||
return res;
|
||||
};
|
||||
window.fetch.__tatWrapped = true;
|
||||
}
|
||||
Reference in New Issue
Block a user