Introduce stable server IDs for subscription state
This commit is contained in:
@@ -23,7 +23,12 @@ import {
|
||||
restoreSingboxConfig,
|
||||
writeSingboxConfig,
|
||||
} from './singbox.js';
|
||||
import { fetchSubscription, getHwid, selectRefreshedServer } from './subscription.js';
|
||||
import {
|
||||
fetchSubscription,
|
||||
getHwid,
|
||||
normalizeSubscriptionConfig,
|
||||
selectRefreshedServer,
|
||||
} from './subscription.js';
|
||||
import {
|
||||
createStateSnapshot,
|
||||
normalizeStoredState,
|
||||
@@ -53,7 +58,9 @@ function readSubscriptionCache() {
|
||||
cacheRecoveryLogged = true;
|
||||
console.warn(`[storage] corrupt subscription cache recovered; backup: ${subscriptionCacheStore.recovery.backupPath}`);
|
||||
}
|
||||
return cached;
|
||||
return cached?.config
|
||||
? { ...cached, ...normalizeSubscriptionConfig(cached.config), _persisted: cached }
|
||||
: cached;
|
||||
}
|
||||
|
||||
const initialStoredState = stateStore.read();
|
||||
@@ -195,10 +202,10 @@ function subscriptionHost(url) {
|
||||
|
||||
function buildActiveConfig(
|
||||
subscriptionConfig,
|
||||
selectedTag,
|
||||
selectedServerId,
|
||||
routeRules = stateStore.read().routeRules,
|
||||
) {
|
||||
return buildGatewayConfig(subscriptionConfig, selectedTag, {
|
||||
return buildGatewayConfig(subscriptionConfig, selectedServerId, {
|
||||
clientDirect: settings.appMode === 'client' && gatewayAutoState.mode === 'gateway-direct',
|
||||
routeRules,
|
||||
});
|
||||
@@ -233,8 +240,8 @@ async function publicState() {
|
||||
function writeCurrentConfig() {
|
||||
const state = stateStore.read();
|
||||
const cached = readSubscriptionCache();
|
||||
if (!state.selectedTag || !cached?.config) return false;
|
||||
writeSingboxConfig(buildActiveConfig(cached.config, state.selectedTag));
|
||||
if (!state.selectedServerId || !cached?.config) return false;
|
||||
writeSingboxConfig(buildActiveConfig(cached.config, state.selectedServerId));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -356,15 +363,15 @@ function refreshGatewayAutoMode({ reconfigure = true } = {}) {
|
||||
return gatewayDiscoveryPromise;
|
||||
}
|
||||
|
||||
async function applySelectedServer(selectedTag, { persist = true } = {}) {
|
||||
async function applySelectedServer(selectedServerId, { persist = true } = {}) {
|
||||
const cached = readSubscriptionCache();
|
||||
if (!cached?.config) throw new HarborError('CONFIG_INVALID');
|
||||
const nextConfig = buildActiveConfig(cached.config, selectedTag);
|
||||
const nextConfig = buildActiveConfig(cached.config, selectedServerId);
|
||||
|
||||
if (persist) {
|
||||
updateStoredState((state) => ({
|
||||
...state,
|
||||
selectedTag,
|
||||
selectedServerId,
|
||||
connectionDesired: 'running',
|
||||
}));
|
||||
}
|
||||
@@ -383,7 +390,7 @@ async function applySelectedServer(selectedTag, { persist = true } = {}) {
|
||||
updateStoredState((state) => ({
|
||||
...state,
|
||||
...(persist ? {
|
||||
appliedTag: selectedTag,
|
||||
appliedServerId: selectedServerId,
|
||||
appliedAt: new Date().toISOString(),
|
||||
} : {}),
|
||||
appliedRouteRules: state.routeRules,
|
||||
@@ -393,7 +400,7 @@ async function applySelectedServer(selectedTag, { persist = true } = {}) {
|
||||
async function applyRouteRules(routeRules) {
|
||||
const state = normalizeStoredState(stateStore.read());
|
||||
const cached = readSubscriptionCache();
|
||||
if (!state.selectedTag || !cached?.config) {
|
||||
if (!state.selectedServerId || !cached?.config) {
|
||||
updateStoredState((current) => ({
|
||||
...current,
|
||||
routeRules,
|
||||
@@ -407,7 +414,7 @@ async function applyRouteRules(routeRules) {
|
||||
: null;
|
||||
const wasRunning = Boolean((await singboxRuntime.refresh()).running);
|
||||
try {
|
||||
writeSingboxConfig(buildActiveConfig(cached.config, state.selectedTag, routeRules));
|
||||
writeSingboxConfig(buildActiveConfig(cached.config, state.selectedServerId, routeRules));
|
||||
if (wasRunning) await startSingbox();
|
||||
updateStoredState((current) => ({
|
||||
...current,
|
||||
@@ -438,11 +445,15 @@ async function commitSubscription(subscriptionUrl, parsed, { resetSelection = fa
|
||||
throw new HarborError('STATE_CONFLICT');
|
||||
}
|
||||
|
||||
const selectedTag = resetSelection
|
||||
const selectedServerId = resetSelection
|
||||
? ''
|
||||
: selectRefreshedServer(previousState.selectedTag, parsed.servers);
|
||||
const candidateConfig = selectedTag
|
||||
? buildActiveConfig(parsed.config, selectedTag, previousState.routeRules)
|
||||
: selectRefreshedServer(
|
||||
previousState.selectedServerId,
|
||||
previousState.servers,
|
||||
parsed.servers,
|
||||
);
|
||||
const candidateConfig = selectedServerId
|
||||
? buildActiveConfig(parsed.config, selectedServerId, previousState.routeRules)
|
||||
: null;
|
||||
const previousCache = readSubscriptionCache();
|
||||
const previousConfig = fs.existsSync(settings.configPath)
|
||||
@@ -452,10 +463,16 @@ async function commitSubscription(subscriptionUrl, parsed, { resetSelection = fa
|
||||
const wasRunning = Boolean((await singboxRuntime.refresh()).running);
|
||||
|
||||
try {
|
||||
if (resetSelection && wasRunning) await stopSingbox();
|
||||
if ((resetSelection || !candidateConfig) && wasRunning) await stopSingbox();
|
||||
if (candidateConfig) writeSingboxConfig(candidateConfig);
|
||||
else removeSingboxConfig();
|
||||
subscriptionCacheStore.write({ url: subscriptionUrl, ...parsed });
|
||||
subscriptionCacheStore.write({
|
||||
url: subscriptionUrl,
|
||||
config: parsed.sourceConfig || parsed.config,
|
||||
servers: parsed.servers,
|
||||
userInfo: parsed.userInfo,
|
||||
fetchedAt: parsed.fetchedAt,
|
||||
});
|
||||
if (!resetSelection && wasRunning && candidateConfig) await startSingbox();
|
||||
|
||||
updateStoredState((state) => ({
|
||||
@@ -468,13 +485,14 @@ async function commitSubscription(subscriptionUrl, parsed, { resetSelection = fa
|
||||
servers: parsed.servers,
|
||||
userInfo: parsed.userInfo,
|
||||
fetchedAt: parsed.fetchedAt,
|
||||
selectedTag,
|
||||
appliedTag: selectedTag,
|
||||
selectedServerId,
|
||||
appliedServerId: selectedServerId,
|
||||
...(!selectedServerId ? { connectionDesired: 'stopped' } : {}),
|
||||
}));
|
||||
if (resetSelection) gatewayAutoState = createGatewayAutoState();
|
||||
} catch (error) {
|
||||
gatewayAutoState = previousGatewayAutoState;
|
||||
if (previousCache) subscriptionCacheStore.write(previousCache);
|
||||
if (previousCache) subscriptionCacheStore.write(previousCache._persisted || previousCache);
|
||||
else subscriptionCacheStore.remove();
|
||||
if (previousConfig === null) removeSingboxConfig();
|
||||
else restoreSingboxConfig(previousConfig);
|
||||
@@ -495,7 +513,8 @@ async function commitSubscription(subscriptionUrl, parsed, { resetSelection = fa
|
||||
servers: parsed.servers,
|
||||
userInfo: parsed.userInfo,
|
||||
fetchedAt: parsed.fetchedAt,
|
||||
selectedTag,
|
||||
selectedServerId,
|
||||
selectedTag: parsed.servers.find((server) => server.id === selectedServerId)?.label || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -559,8 +578,9 @@ async function handleApi(req, res) {
|
||||
if (req.method === 'POST' && req.url === '/api/servers/ping-all') {
|
||||
const state = stateStore.read();
|
||||
const results = await Promise.all((state.servers || []).map(async (server) => ({
|
||||
tag: String(server.tag || '').trim(),
|
||||
...await tcpPing(server.server, server.server_port),
|
||||
id: server.id,
|
||||
tag: server.label,
|
||||
...await tcpPing(server.host, server.port),
|
||||
checkedAt: new Date().toISOString(),
|
||||
})));
|
||||
return sendState(res, { results });
|
||||
@@ -644,11 +664,20 @@ async function handleApi(req, res) {
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/api/apply') {
|
||||
const { selectedTag = '' } = await readBody(req);
|
||||
const tag = String(selectedTag).trim();
|
||||
if (!tag) throw new HarborError('REQUEST_INVALID');
|
||||
await withOperation('apply-server', () => serializeControl(() => applySelectedServer(tag)));
|
||||
return sendState(res, { selectedTag: tag });
|
||||
const { serverId = '', selectedTag = '' } = await readBody(req);
|
||||
const state = normalizeStoredState(stateStore.read());
|
||||
const id = String(serverId).trim() || (() => {
|
||||
const matches = state.servers.filter((server) => server.label === String(selectedTag).trim());
|
||||
return matches.length === 1 ? matches[0].id : '';
|
||||
})();
|
||||
if (!id || !state.servers.some((server) => server.id === id)) {
|
||||
throw new HarborError('SERVER_NOT_FOUND');
|
||||
}
|
||||
await withOperation('apply-server', () => serializeControl(() => applySelectedServer(id)));
|
||||
return sendState(res, {
|
||||
serverId: id,
|
||||
selectedTag: state.servers.find((server) => server.id === id)?.label || '',
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/api/singbox/stop') {
|
||||
@@ -667,7 +696,7 @@ async function handleApi(req, res) {
|
||||
await singboxRuntime.restart();
|
||||
updateStoredState((state) => ({
|
||||
...state,
|
||||
appliedTag: state.selectedTag,
|
||||
appliedServerId: state.selectedServerId,
|
||||
connectionDesired: 'running',
|
||||
appliedRouteRules: state.routeRules,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user