Skip to content

Commit 215ba34

Browse files
authored
Avoid blocking account config on status reads (#24)
1 parent 31ce468 commit 215ba34

4 files changed

Lines changed: 45 additions & 8 deletions

File tree

tests/strategy_switch_worker_validation.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ const crossOriginError = captureError(
4343
assert.match(crossOriginError.message, /cross-origin request rejected/);
4444
assert.equal(crossOriginError.status, 403);
4545

46+
assert.equal(
47+
await __test.withTimeout(new Promise((resolve) => setTimeout(() => resolve("late"), 25)), 1, "fallback"),
48+
"fallback",
49+
);
50+
4651
function captureError(fn) {
4752
try {
4853
fn();

web/strategy-switch-console/index.html

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -746,9 +746,11 @@ <h2 data-i18n="summary">切换摘要</h2>
746746
summary: "当前配置状态",
747747
copySummary: "复制状态",
748748
loginToRun: "登录后切换",
749+
loadingConfig: "读取配置中",
749750
configureAccounts: "配置账号后切换",
750751
runSwitch: "一键切换",
751752
readonlyNote: "登录后才可执行切换。",
753+
loadingConfigNote: "正在读取账号配置和当前状态。",
752754
missingConfigNote: "账号配置未加载,暂时不能执行。",
753755
readyNote: "点击后会触发 workflow,并同步目标平台服务。",
754756
invalidStrategyNote: "当前账号没有可执行策略,暂时不能切换。",
@@ -797,9 +799,11 @@ <h2 data-i18n="summary">切换摘要</h2>
797799
summary: "Current Config",
798800
copySummary: "Copy state",
799801
loginToRun: "Sign in to switch",
802+
loadingConfig: "Loading config",
800803
configureAccounts: "Configure accounts",
801804
runSwitch: "Switch now",
802805
readonlyNote: "Sign in to switch.",
806+
loadingConfigNote: "Reading account config and current state.",
803807
missingConfigNote: "Account config is not loaded, so switching is disabled.",
804808
readyNote: "This dispatches the workflow and syncs the target platform service.",
805809
invalidStrategyNote: "This account has no runnable strategy, so switching is disabled.",
@@ -1108,7 +1112,9 @@ <h2 data-i18n="summary">切换摘要</h2>
11081112
const currentProfile = currentStrategyForAccount(state.selected, account);
11091113
const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);
11101114
const source = currentEntry?.source
1111-
|| (state.configSource === "private" ? t("accountConfigLoaded") : t("publicPreview"));
1115+
|| (state.configSource === "loading"
1116+
? t("loadingConfig")
1117+
: (state.configSource === "private" ? t("accountConfigLoaded") : t("publicPreview")));
11121118
return [
11131119
[t("repository"), repositories[state.selected]],
11141120
[t("selectedAccount"), account.label],
@@ -1242,16 +1248,19 @@ <h2 data-i18n="summary">切换摘要</h2>
12421248

12431249
const dispatch = el("dispatch-button");
12441250
const hasPrivateAccounts = state.configSource === "private";
1251+
const loadingConfig = state.configSource === "loading";
12451252
const hasValidStrategy = hasValidStrategySelection();
1246-
dispatch.disabled = !state.auth.allowed || !hasPrivateAccounts || !hasValidStrategy;
1253+
dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy;
12471254
dispatch.textContent = state.auth.allowed
1248-
? (hasPrivateAccounts ? t("runSwitch") : t("configureAccounts"))
1255+
? (loadingConfig ? t("loadingConfig") : (hasPrivateAccounts ? t("runSwitch") : t("configureAccounts")))
12491256
: t("loginToRun");
12501257
const note = el("action-note");
12511258
note.textContent = state.auth.allowed
1252-
? (hasPrivateAccounts ? (hasValidStrategy ? t("readyNote") : t("invalidStrategyNote")) : t("missingConfigNote"))
1259+
? (loadingConfig
1260+
? t("loadingConfigNote")
1261+
: (hasPrivateAccounts ? (hasValidStrategy ? t("readyNote") : t("invalidStrategyNote")) : t("missingConfigNote")))
12531262
: t("readonlyNote");
1254-
note.classList.toggle("warning", state.auth.allowed && (!hasPrivateAccounts || !hasValidStrategy));
1263+
note.classList.toggle("warning", state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy));
12551264
}
12561265

12571266
function render() {
@@ -1296,6 +1305,8 @@ <h2 data-i18n="summary">切换摘要</h2>
12961305

12971306
async function refreshConfig() {
12981307
if (!state.auth.available || !state.auth.allowed) return;
1308+
state.configSource = "loading";
1309+
render();
12991310
try {
13001311
const response = await fetch("/api/config", { cache: "no-store" });
13011312
if (!response.ok) throw new Error("no config");

web/strategy-switch-console/page_asset.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/strategy-switch-console/worker.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const ACCOUNT_OPTIONS_KEY = "account_options";
1111
const STRATEGY_PROFILES_KEY = "strategy_profiles";
1212
const AUDIT_LOG_KEY = "audit_log";
1313
const AUDIT_LOG_LIMIT = 50;
14+
const CURRENT_STRATEGIES_TIMEOUT_MS = 3500;
1415

1516
const SUPPORTED_PLATFORMS = ["longbridge", "ibkr", "schwab", "firstrade"];
1617
const SUPPORTED_STRATEGY_DOMAINS = ["us_equity", "hk_equity"];
@@ -452,11 +453,10 @@ async function configPayload(request, env) {
452453
if (!session?.allowed) return { accountOptions: null };
453454
const accountConfig = await loadAccountOptionsConfig(env);
454455
const strategyProfiles = await loadStrategyProfilesConfig(env);
455-
const currentStrategies = await loadCurrentStrategies(accountConfig.options, env);
456456
return {
457457
accountOptions: accountConfig.options,
458458
strategyProfiles,
459-
currentStrategies,
459+
currentStrategies: await loadCurrentStrategiesSafely(accountConfig.options, env),
460460
};
461461
}
462462

@@ -503,6 +503,26 @@ async function loadCurrentStrategies(accountOptions, env) {
503503
return currentStrategies;
504504
}
505505

506+
async function loadCurrentStrategiesSafely(accountOptions, env) {
507+
try {
508+
return await withTimeout(
509+
loadCurrentStrategies(accountOptions, env),
510+
CURRENT_STRATEGIES_TIMEOUT_MS,
511+
{},
512+
);
513+
} catch {
514+
return {};
515+
}
516+
}
517+
518+
function withTimeout(promise, timeoutMs, fallback) {
519+
let timeoutId;
520+
const timeout = new Promise((resolve) => {
521+
timeoutId = setTimeout(() => resolve(fallback), timeoutMs);
522+
});
523+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timeoutId));
524+
}
525+
506526
async function resolveCurrentStrategyForAccount({ platform, option, optionsCount, repository, readVariable }) {
507527
const serviceTargetsValue = await readVariable(repository, "repository", "", "CLOUD_RUN_SERVICE_TARGETS_JSON");
508528
const serviceTarget = runtimeTargetFromServiceTargets(serviceTargetsValue, platform, option);
@@ -1488,4 +1508,5 @@ export const __test = {
14881508
requireSameOrigin,
14891509
responseHeaders,
14901510
supportedDomainsForAccount,
1511+
withTimeout,
14911512
};

0 commit comments

Comments
 (0)