Track applied route rules separately from pending edits
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 16s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-07-11 23:07:20 +03:00
parent 65bf88bf41
commit f135ade43b
9 changed files with 215 additions and 49 deletions

View File

@@ -380,16 +380,14 @@ async function applySelectedServer(selectedTag, { persist = true } = {}) {
else restoreSingboxConfig(previousConfig);
throw error;
}
if (persist || stateStore.read().routeRulesPendingRestart) {
updateStoredState((state) => ({
...state,
...(persist ? {
appliedTag: selectedTag,
appliedAt: new Date().toISOString(),
} : {}),
routeRulesPendingRestart: false,
}));
}
updateStoredState((state) => ({
...state,
...(persist ? {
appliedTag: selectedTag,
appliedAt: new Date().toISOString(),
} : {}),
appliedRouteRules: state.routeRules,
}));
}
async function applyRouteRules(routeRules) {
@@ -399,7 +397,7 @@ async function applyRouteRules(routeRules) {
updateStoredState((current) => ({
...current,
routeRules,
routeRulesPendingRestart: true,
routeRulesRevision: current.routeRulesRevision + 1,
}));
return;
}
@@ -414,7 +412,8 @@ async function applyRouteRules(routeRules) {
updateStoredState((current) => ({
...current,
routeRules,
routeRulesPendingRestart: !wasRunning,
...(wasRunning ? { appliedRouteRules: routeRules } : {}),
routeRulesRevision: current.routeRulesRevision + 1,
}));
} catch (error) {
if (previousConfig === null) removeSingboxConfig();
@@ -611,19 +610,23 @@ async function handleApi(req, res) {
}
if (req.method === 'PUT' && req.url === '/api/route-rules') {
const { rules, expectedRevision } = await readBody(req);
const { rules, expectedRulesRevision, expectedRevision } = await readBody(req);
let routeRules;
try {
routeRules = normalizeRouteRules(rules, { strict: true });
} catch (cause) {
throw new HarborError('REQUEST_INVALID', { cause });
}
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
const rulesRevision = expectedRulesRevision ?? expectedRevision;
if (!Number.isSafeInteger(rulesRevision) || rulesRevision < 0) {
throw new HarborError('REQUEST_INVALID');
}
await serializeControl(async () => {
const current = normalizeStoredState(stateStore.read());
if (current.revision !== expectedRevision) throw new HarborError('STATE_CONFLICT');
const currentRevision = expectedRulesRevision == null
? current.revision
: current.routeRulesRevision;
if (currentRevision !== rulesRevision) throw new HarborError('STATE_CONFLICT');
if (isDeepStrictEqual(current.routeRules, routeRules)) return;
await withOperation('route-rules', () => applyRouteRules(routeRules));
});
@@ -667,7 +670,7 @@ async function handleApi(req, res) {
...state,
appliedTag: state.selectedTag,
connectionDesired: 'running',
routeRulesPendingRestart: false,
appliedRouteRules: state.routeRules,
}));
}));
return sendState(res, { singboxRunning: true });
@@ -727,8 +730,8 @@ if (settings.appMode === 'client' || !fs.existsSync(settings.configPath)) {
}
await startSingbox()
.then(() => {
if (fs.existsSync(settings.configPath) && stateStore.read().routeRulesPendingRestart) {
updateStoredState((state) => ({ ...state, routeRulesPendingRestart: false }));
if (fs.existsSync(settings.configPath)) {
updateStoredState((state) => ({ ...state, appliedRouteRules: state.routeRules }));
}
})
.catch((error) => console.warn(`[control] sing-box не запущен: ${error.message}`));