Introduce stable server IDs for subscription state
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { normalizeRouteRules } from '../routingRules.js';
|
||||
import { normalizeServers, resolveServerId } from '../serverIdentity.js';
|
||||
|
||||
const MODES = new Set(['client', 'gateway']);
|
||||
const CONNECTION_STATES = new Set(['running', 'stopped']);
|
||||
@@ -12,13 +13,21 @@ const dateOrNull = (value) => (
|
||||
|
||||
export function normalizeStoredState(value) {
|
||||
const state = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
||||
const selectedTag = text(state.selectedTag);
|
||||
const servers = normalizeServers(state.servers);
|
||||
const selectedServerId = resolveServerId(servers, state.selectedServerId, state.selectedTag);
|
||||
const appliedServerId = Object.hasOwn(state, 'appliedServerId')
|
||||
? resolveServerId(servers, state.appliedServerId)
|
||||
: resolveServerId(servers, '', state.appliedTag || state.selectedTag);
|
||||
const selectedServer = servers.find((server) => server.id === selectedServerId);
|
||||
const appliedServer = servers.find((server) => server.id === appliedServerId);
|
||||
return {
|
||||
...state,
|
||||
revision: Number.isSafeInteger(state.revision) && state.revision >= 0 ? state.revision : 0,
|
||||
selectedTag,
|
||||
appliedTag: Object.hasOwn(state, 'appliedTag') ? text(state.appliedTag) : selectedTag,
|
||||
servers: Array.isArray(state.servers) ? state.servers : [],
|
||||
selectedServerId,
|
||||
appliedServerId,
|
||||
selectedTag: selectedServer?.label || '',
|
||||
appliedTag: appliedServer?.label || '',
|
||||
servers,
|
||||
routeRules: normalizeRouteRules(state.routeRules),
|
||||
appliedRouteRules: normalizeRouteRules(state.appliedRouteRules),
|
||||
routeRulesRevision: Number.isSafeInteger(state.routeRulesRevision) && state.routeRulesRevision >= 0
|
||||
@@ -43,22 +52,7 @@ export function createStateSnapshot({
|
||||
const desired = CONNECTION_STATES.has(stored.connectionDesired)
|
||||
? stored.connectionDesired
|
||||
: configExists ? 'running' : 'stopped';
|
||||
const servers = stored.servers.map((server) => {
|
||||
const tag = text(server.tag);
|
||||
return {
|
||||
...server,
|
||||
id: tag,
|
||||
label: tag,
|
||||
host: text(server.server),
|
||||
port: Number(server.server_port) || 0,
|
||||
protocol: text(server.type),
|
||||
// v0 compatibility: the current UI and integrations still read these aliases.
|
||||
tag,
|
||||
server: text(server.server),
|
||||
server_port: Number(server.server_port) || 0,
|
||||
type: text(server.type),
|
||||
};
|
||||
});
|
||||
const servers = stored.servers;
|
||||
const routeMode = mode === 'client' ? gatewayAuto?.mode || 'local-vpn' : 'gateway-transparent';
|
||||
const activeLocalRules = runtime?.running ? stored.appliedRouteRules : [];
|
||||
|
||||
@@ -74,8 +68,8 @@ export function createStateSnapshot({
|
||||
userInfo: stored.userInfo && typeof stored.userInfo === 'object' ? stored.userInfo : {},
|
||||
},
|
||||
selection: {
|
||||
desiredServerId: stored.selectedTag,
|
||||
appliedServerId: stored.appliedTag,
|
||||
desiredServerId: stored.selectedServerId,
|
||||
appliedServerId: stored.appliedServerId,
|
||||
},
|
||||
connection: {
|
||||
desired,
|
||||
@@ -120,7 +114,7 @@ export function withStateV0Compatibility(snapshot, {
|
||||
singboxStartedAt: snapshot.connection.startedAt,
|
||||
subscriptionHost: snapshot.subscription.host,
|
||||
hasSubscription: snapshot.subscription.status === 'ready',
|
||||
selectedTag: snapshot.selection.desiredServerId,
|
||||
selectedTag: stored.selectedTag,
|
||||
userInfo: snapshot.subscription.userInfo,
|
||||
fetchedAt: snapshot.subscription.fetchedAt,
|
||||
gatewayAuto: snapshot.mode === 'client' ? {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
const text = (value) => String(value || '').trim();
|
||||
|
||||
function hash64(value) {
|
||||
let hash = 0xcbf29ce484222325n;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
hash ^= BigInt(value.charCodeAt(index));
|
||||
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
||||
}
|
||||
return hash.toString(16).padStart(16, '0');
|
||||
}
|
||||
|
||||
export function serverIdentityKey(server) {
|
||||
const protocol = text(server?.protocol || server?.type).toLowerCase();
|
||||
const host = text(server?.host || server?.server).toLowerCase();
|
||||
const port = Number(server?.port || server?.server_port) || 0;
|
||||
return `${protocol}\u0000${host}\u0000${port}`;
|
||||
}
|
||||
|
||||
export function createServerId(server) {
|
||||
return `srv_${hash64(`endpoint\u0000${serverIdentityKey(server)}`)}`;
|
||||
}
|
||||
|
||||
export function normalizeServer(server) {
|
||||
const source = server && typeof server === 'object' ? server : {};
|
||||
const protocol = text(source.protocol || source.type).toLowerCase();
|
||||
const host = text(source.host || source.server);
|
||||
const port = Number(source.port || source.server_port) || 0;
|
||||
const id = text(source.id) || createServerId(source);
|
||||
const label = text(source.label || source.tag) || host;
|
||||
const metadata = Object.fromEntries([
|
||||
['country', text(source.country)],
|
||||
['city', text(source.city)],
|
||||
['provider', text(source.provider)],
|
||||
].filter(([, value]) => value));
|
||||
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
host,
|
||||
port,
|
||||
protocol,
|
||||
...metadata,
|
||||
// v0 aliases remain until old clients no longer consume this API.
|
||||
tag: label,
|
||||
server: host,
|
||||
server_port: port,
|
||||
type: protocol,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeServers(servers) {
|
||||
const seen = new Set();
|
||||
return (Array.isArray(servers) ? servers : []).flatMap((server) => {
|
||||
const normalized = normalizeServer(server);
|
||||
if (!normalized.id || seen.has(normalized.id)) return [];
|
||||
seen.add(normalized.id);
|
||||
return [normalized];
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveServerId(servers, serverId, legacyTag = '') {
|
||||
const id = text(serverId);
|
||||
if (id) return servers.some((server) => server.id === id) ? id : '';
|
||||
const tag = text(legacyTag);
|
||||
const matches = tag ? servers.filter((server) => server.label === tag) : [];
|
||||
return matches.length === 1 ? matches[0].id : '';
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export const HARBOR_VERSIONS = Object.freeze({
|
||||
macClient: '0.6.17',
|
||||
gatewayClient: '0.6.16',
|
||||
gatewayBackend: '0.6.1',
|
||||
macClient: '0.7.0',
|
||||
gatewayClient: '0.7.0',
|
||||
gatewayBackend: '0.7.0',
|
||||
});
|
||||
|
||||
export function parseVersion(value) {
|
||||
|
||||
Reference in New Issue
Block a user