Refine subscription import and refresh flow
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 15s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-07-12 11:18:33 +03:00
parent ba15a25c89
commit 005c7a101b
15 changed files with 190 additions and 218 deletions

View File

@@ -431,55 +431,39 @@ async function applyRouteRules(routeRules) {
}
}
function refreshSavedSubscription() {
if (subscriptionRefreshPromise) return subscriptionRefreshPromise;
subscriptionRefreshPromise = (async () => {
const initialState = stateStore.read();
if (!initialState.subscriptionUrl) {
throw new HarborError('SUBSCRIPTION_INVALID');
async function commitSubscription(subscriptionUrl, parsed, { resetSelection = false } = {}) {
return serializeControl(async () => {
const previousState = normalizeStoredState(stateStore.read());
if (!resetSelection && previousState.subscriptionUrl !== subscriptionUrl) {
throw new HarborError('STATE_CONFLICT');
}
const subscriptionUrl = initialState.subscriptionUrl;
const parsed = await fetchSubscription(subscriptionUrl);
return serializeControl(async () => {
const currentState = stateStore.read();
if (currentState.subscriptionUrl !== subscriptionUrl) {
throw new HarborError('STATE_CONFLICT');
}
const selectedTag = resetSelection
? ''
: selectRefreshedServer(previousState.selectedTag, parsed.servers);
const candidateConfig = selectedTag
? buildActiveConfig(parsed.config, selectedTag, previousState.routeRules)
: null;
const previousCache = readSubscriptionCache();
const previousConfig = fs.existsSync(settings.configPath)
? fs.readFileSync(settings.configPath, 'utf8')
: null;
const previousGatewayAutoState = gatewayAutoState;
const wasRunning = Boolean((await singboxRuntime.refresh()).running);
const selectedTag = selectRefreshedServer(currentState.selectedTag, parsed.servers);
const previousCache = readSubscriptionCache();
const previousConfig = fs.existsSync(settings.configPath)
? fs.readFileSync(settings.configPath, 'utf8')
: null;
const activeConfigChanged = Boolean(currentState.selectedTag && selectedTag) && (
!previousCache?.config || !isDeepStrictEqual(
buildActiveConfig(previousCache.config, currentState.selectedTag),
buildActiveConfig(parsed.config, selectedTag),
)
);
try {
if (resetSelection && wasRunning) await stopSingbox();
if (candidateConfig) writeSingboxConfig(candidateConfig);
else removeSingboxConfig();
subscriptionCacheStore.write({ url: subscriptionUrl, ...parsed });
try {
if (singboxRuntime.running && activeConfigChanged) {
await applySelectedServer(selectedTag, { persist: false });
}
else if (selectedTag) {
if (!singboxRuntime.running) writeSingboxConfig(buildActiveConfig(parsed.config, selectedTag));
} else {
removeSingboxConfig();
}
} catch (error) {
if (previousCache) subscriptionCacheStore.write(previousCache);
else subscriptionCacheStore.remove();
if (previousConfig === null) removeSingboxConfig();
else restoreSingboxConfig(previousConfig);
throw error;
}
if (!resetSelection && wasRunning && candidateConfig) await startSingbox();
updateStoredState((state) => ({
...state,
...(resetSelection ? {
routeRules: state.routeRules,
gatewayAutoEnabled: state.gatewayAutoEnabled !== false,
connectionDesired: 'stopped',
} : state),
subscriptionUrl,
servers: parsed.servers,
userInfo: parsed.userInfo,
@@ -487,15 +471,48 @@ function refreshSavedSubscription() {
selectedTag,
appliedTag: selectedTag,
}));
if (resetSelection) gatewayAutoState = createGatewayAutoState();
} catch (error) {
gatewayAutoState = previousGatewayAutoState;
if (previousCache) subscriptionCacheStore.write(previousCache);
else subscriptionCacheStore.remove();
if (previousConfig === null) removeSingboxConfig();
else restoreSingboxConfig(previousConfig);
if (wasRunning) {
try {
await startSingbox();
} catch (rollbackError) {
throw new HarborError('PROCESS_START_FAILED', {
cause: new AggregateError([error, rollbackError], 'Subscription rollback failed'),
});
}
}
throw error;
}
return {
success: true,
servers: parsed.servers,
userInfo: parsed.userInfo,
fetchedAt: parsed.fetchedAt,
selectedTag,
};
});
return {
success: true,
servers: parsed.servers,
userInfo: parsed.userInfo,
fetchedAt: parsed.fetchedAt,
selectedTag,
};
});
}
async function importSubscription(subscriptionUrl) {
const parsed = await fetchSubscription(subscriptionUrl);
return commitSubscription(subscriptionUrl, parsed, { resetSelection: true });
}
function refreshSavedSubscription() {
if (subscriptionRefreshPromise) return subscriptionRefreshPromise;
subscriptionRefreshPromise = (async () => {
const subscriptionUrl = stateStore.read().subscriptionUrl;
if (!subscriptionUrl) throw new HarborError('SUBSCRIPTION_INVALID');
const parsed = await fetchSubscription(subscriptionUrl);
return commitSubscription(subscriptionUrl, parsed);
})().finally(() => {
subscriptionRefreshPromise = null;
});
@@ -553,25 +570,7 @@ async function handleApi(req, res) {
const { url = '' } = await readBody(req);
const normalizedUrl = String(url).trim();
const parsed = await withOperation('subscription-import', async () => {
const result = await fetchSubscription(normalizedUrl);
await serializeControl(async () => {
await stopSingbox();
removeSingboxConfig();
subscriptionCacheStore.write({ url: normalizedUrl, ...result });
updateStoredState((state) => ({
routeRules: state.routeRules,
subscriptionUrl: normalizedUrl,
gatewayAutoEnabled: state.gatewayAutoEnabled !== false,
servers: result.servers,
userInfo: result.userInfo,
fetchedAt: result.fetchedAt,
selectedTag: '',
appliedTag: '',
connectionDesired: 'stopped',
}));
gatewayAutoState = createGatewayAutoState();
});
return result;
return importSubscription(normalizedUrl);
});
return sendState(res, parsed);
}