diff --git a/src/localization/en.json b/src/localization/en.json
index a7a15bc2..fe8fac48 100644
--- a/src/localization/en.json
+++ b/src/localization/en.json
@@ -2350,7 +2350,8 @@
"vpn_in_use": "VRChat currently blocks most VPNs. Please disable any connected VPNs and try again.",
"login_error": "Login Error",
"invalid_json_response": "Invalid JSON response",
- "403_404_bailing_request": "Bailing request due to recent 404/403"
+ "403_404_bailing_request": "Bailing request due to recent 404/403",
+ "unavailable": "Service may be unavailable due to VRChat internal issues"
}
}
}
diff --git a/src/localization/ja.json b/src/localization/ja.json
index 5cdd41fd..a32db581 100644
--- a/src/localization/ja.json
+++ b/src/localization/ja.json
@@ -2223,7 +2223,8 @@
"vpn_in_use": "VRChatは現在ほとんどのVPNをブロックしています。接続中のVPNを無効にして、もう一度お試しください。",
"login_error": "ログインエラー",
"invalid_json_response": "無効なJSONレスポンス",
- "403_404_bailing_request": "最近の404/403エラーのため、リクエストを中止しました"
+ "403_404_bailing_request": "最近の404/403エラーのため、リクエストを中止しました",
+ "unavailable": "サービスはVRChatの内部問題により利用できない可能性があります"
}
}
}
diff --git a/src/localization/zh-CN.json b/src/localization/zh-CN.json
index 734fe9db..b811351b 100644
--- a/src/localization/zh-CN.json
+++ b/src/localization/zh-CN.json
@@ -2349,7 +2349,8 @@
"vpn_in_use": "VRChat 目前限制了大多数的 VPN 服务。请断开所有已连接的 VPN 后重试。",
"login_error": "登录失败",
"invalid_json_response": "无效的 JSON 响应",
- "403_404_bailing_request": "由于最近出现的 403/404 错误,请求已中止"
+ "403_404_bailing_request": "由于最近出现的 403/404 错误,请求已中止",
+ "unavailable": "服务可能由于 VRChat 内部问题而不可用"
}
}
}
diff --git a/src/service/request.js b/src/service/request.js
index b2445993..50722f91 100644
--- a/src/service/request.js
+++ b/src/service/request.js
@@ -284,7 +284,6 @@ export function $throw(code, error, endpoint) {
`${t('api.error.message.endpoint')}: "${typeof endpoint === 'string' ? endpoint : JSON.stringify(endpoint)}"`
);
}
- const text = message.map((s) => escapeTag(s)).join('
');
let ignoreError = false;
if (
(code === 404 || code === -1) &&
@@ -298,11 +297,13 @@ export function $throw(code, error, endpoint) {
) {
ignoreError = true;
}
- if (
- (code === 403 || code === 404 || code === -1) &&
- endpoint?.startsWith('instances/')
- ) {
- ignoreError = true;
+ if (code === 403 || code === 404 || code === -1) {
+ if (endpoint?.startsWith('instances/')) {
+ ignoreError = true;
+ }
+ if (endpoint?.includes('/mutuals/friends')) {
+ message[1] = `${t('api.error.message.error_message')}: "${t('api.error.message.unavailable')}"`;
+ }
}
if (endpoint?.startsWith('analysis/')) {
ignoreError = true;
@@ -310,6 +311,8 @@ export function $throw(code, error, endpoint) {
if (endpoint.endsWith('/mutuals') && (code === 403 || code === -1)) {
ignoreError = true;
}
+ const text = message.map((s) => escapeTag(s)).join('
');
+
if (text.length && !ignoreError) {
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
diff --git a/src/shared/utils/user.js b/src/shared/utils/user.js
index 278bcc73..af92910c 100644
--- a/src/shared/utils/user.js
+++ b/src/shared/utils/user.js
@@ -259,16 +259,16 @@ function parseUserUrl(user) {
/**
*
- * @param {object} ctx
+ * @param {object} ref
* @returns {string}
*/
-function userOnlineFor(ctx) {
- if (ctx.ref.state === 'online' && ctx.ref.$online_for) {
- return timeToText(Date.now() - ctx.ref.$online_for);
- } else if (ctx.ref.state === 'active' && ctx.ref.$active_for) {
- return timeToText(Date.now() - ctx.ref.$active_for);
- } else if (ctx.ref.$offline_for) {
- return timeToText(Date.now() - ctx.ref.$offline_for);
+function userOnlineFor(ref) {
+ if (ref.state === 'online' && ref.$online_for) {
+ return timeToText(Date.now() - ref.$online_for);
+ } else if (ref.state === 'active' && ref.$active_for) {
+ return timeToText(Date.now() - ref.$active_for);
+ } else if (ref.$offline_for) {
+ return timeToText(Date.now() - ref.$offline_for);
}
return '-';
}
diff --git a/src/views/Charts/components/MutualFriends.vue b/src/views/Charts/components/MutualFriends.vue
index 7499985b..ff815730 100644
--- a/src/views/Charts/components/MutualFriends.vue
+++ b/src/views/Charts/components/MutualFriends.vue
@@ -268,6 +268,8 @@
}
}
+ const isCancelled = () => status.cancelRequested === true;
+
async function startFetch() {
const rateLimiter = createRateLimiter({
limitPerInterval: 5,
@@ -278,12 +280,34 @@
const collected = [];
let offset = 0;
while (true) {
+ if (isCancelled()) {
+ break;
+ }
await rateLimiter.wait();
- const args = await executeWithBackoff(() => userRequest.getMutualFriends({ userId, offset, n: 100 }), {
- maxRetries: 4,
- baseDelay: 500,
- shouldRetry: (err) => err?.status === 429 || (err?.message || '').includes('429')
+ if (isCancelled()) {
+ break;
+ }
+ const args = await executeWithBackoff(
+ () => {
+ if (isCancelled()) {
+ throw new Error('cancelled');
+ }
+ return userRequest.getMutualFriends({ userId, offset, n: 100 });
+ },
+ {
+ maxRetries: 4,
+ baseDelay: 500,
+ shouldRetry: (err) => err?.status === 429 || (err?.message || '').includes('429')
+ }
+ ).catch((err) => {
+ if ((err?.message || '') === 'cancelled') {
+ return null;
+ }
+ throw err;
});
+ if (!args || isCancelled()) {
+ break;
+ }
collected.push(...args.json);
if (args.json.length < 100) {
break;
@@ -320,10 +344,22 @@
if (!friend?.id) {
continue;
}
+ if (isCancelled()) {
+ cancelled = true;
+ break;
+ }
try {
const mutuals = await fetchMutualFriends(friend.id);
+ if (isCancelled()) {
+ cancelled = true;
+ break;
+ }
mutualMap.set(friend.id, { friend, mutuals });
} catch (err) {
+ if ((err?.message || '') === 'cancelled' || isCancelled()) {
+ cancelled = true;
+ break;
+ }
console.warn('[MutualNetworkGraph] Skipping friend due to fetch error', friend.id, err);
continue;
}