mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 01:06:36 +08:00
fix: stabilize federation forward runtime cleanup and wire frontend version update UX
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
export type UpdateReleaseChannel = "stable" | "dev";
|
||||
|
||||
export const UPDATE_CHANNEL_STORAGE_KEY = "update-release-channel";
|
||||
export const UPDATE_CHANNEL_CHANGED_EVENT = "updateReleaseChannelChanged";
|
||||
|
||||
const CHANNEL_STABLE: UpdateReleaseChannel = "stable";
|
||||
const CHANNEL_DEV: UpdateReleaseChannel = "dev";
|
||||
|
||||
const stableVersionPattern = /^\d+(?:\.\d+)+$/;
|
||||
const testKeywordPattern = /(alpha|beta|rc)/i;
|
||||
|
||||
const VERSION_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
|
||||
type ReleaseItem = {
|
||||
tag_name?: string;
|
||||
draft?: boolean;
|
||||
};
|
||||
|
||||
type LatestVersionCacheEntry = {
|
||||
value: string | null;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
const latestVersionCache: Record<
|
||||
UpdateReleaseChannel,
|
||||
LatestVersionCacheEntry
|
||||
> = {
|
||||
stable: { value: null, expiresAt: 0 },
|
||||
dev: { value: null, expiresAt: 0 },
|
||||
};
|
||||
|
||||
const normalizeChannel = (
|
||||
value: string | null | undefined,
|
||||
): UpdateReleaseChannel => {
|
||||
return value === CHANNEL_DEV ? CHANNEL_DEV : CHANNEL_STABLE;
|
||||
};
|
||||
|
||||
export const getUpdateReleaseChannel = (): UpdateReleaseChannel => {
|
||||
if (typeof window === "undefined") {
|
||||
return CHANNEL_STABLE;
|
||||
}
|
||||
|
||||
return normalizeChannel(localStorage.getItem(UPDATE_CHANNEL_STORAGE_KEY));
|
||||
};
|
||||
|
||||
export const setUpdateReleaseChannel = (
|
||||
channel: UpdateReleaseChannel,
|
||||
): void => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(UPDATE_CHANNEL_STORAGE_KEY, normalizeChannel(channel));
|
||||
window.dispatchEvent(new Event(UPDATE_CHANNEL_CHANGED_EVENT));
|
||||
};
|
||||
|
||||
const normalizeTag = (tag: string): string => {
|
||||
return tag.trim().replace(/^v/i, "");
|
||||
};
|
||||
|
||||
type ReleaseTagChannel = UpdateReleaseChannel | null;
|
||||
|
||||
const releaseChannelFromTag = (tag: string): ReleaseTagChannel => {
|
||||
const normalizedTag = normalizeTag(tag).toLowerCase();
|
||||
|
||||
if (!normalizedTag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stableVersionPattern.test(normalizedTag)) {
|
||||
return CHANNEL_STABLE;
|
||||
}
|
||||
|
||||
if (testKeywordPattern.test(normalizedTag)) {
|
||||
return CHANNEL_DEV;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type VersionParts = {
|
||||
numbers: number[];
|
||||
stageRank: number;
|
||||
stageNumber: number;
|
||||
};
|
||||
|
||||
const parseVersionParts = (version: string): VersionParts => {
|
||||
const normalized = normalizeTag(version).toLowerCase();
|
||||
const numberMatches = normalized.match(/\d+/g) || [];
|
||||
const numbers = numberMatches.map((item) => Number.parseInt(item, 10));
|
||||
|
||||
let stageRank = 0;
|
||||
|
||||
if (normalized.includes("rc")) {
|
||||
stageRank = 3;
|
||||
} else if (normalized.includes("beta")) {
|
||||
stageRank = 2;
|
||||
} else if (normalized.includes("alpha")) {
|
||||
stageRank = 1;
|
||||
} else if (stableVersionPattern.test(normalized)) {
|
||||
stageRank = 4;
|
||||
}
|
||||
|
||||
const stageNumberMatch = normalized.match(/(?:alpha|beta|rc)[.-]?(\d+)/);
|
||||
const stageNumber = stageNumberMatch
|
||||
? Number.parseInt(stageNumberMatch[1], 10)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
numbers,
|
||||
stageRank,
|
||||
stageNumber,
|
||||
};
|
||||
};
|
||||
|
||||
export const compareVersions = (left: string, right: string): number => {
|
||||
const a = parseVersionParts(left);
|
||||
const b = parseVersionParts(right);
|
||||
const maxLength = Math.max(a.numbers.length, b.numbers.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i += 1) {
|
||||
const aValue = a.numbers[i] || 0;
|
||||
const bValue = b.numbers[i] || 0;
|
||||
|
||||
if (aValue !== bValue) {
|
||||
return aValue - bValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (a.stageRank !== b.stageRank) {
|
||||
return a.stageRank - b.stageRank;
|
||||
}
|
||||
|
||||
if (a.stageNumber !== b.stageNumber) {
|
||||
return a.stageNumber - b.stageNumber;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const repoPathFromUrl = (repoUrl: string): string | null => {
|
||||
try {
|
||||
const parsed = new URL(repoUrl);
|
||||
const segments = parsed.pathname
|
||||
.replace(/\.git$/i, "")
|
||||
.split("/")
|
||||
.filter(Boolean);
|
||||
|
||||
if (segments.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${segments[0]}/${segments[1]}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestVersionByChannel = async (
|
||||
channel: UpdateReleaseChannel,
|
||||
repoUrl: string,
|
||||
): Promise<string | null> => {
|
||||
const normalizedChannel = normalizeChannel(channel);
|
||||
const now = Date.now();
|
||||
const cached = latestVersionCache[normalizedChannel];
|
||||
|
||||
if (cached.value && cached.expiresAt > now) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
const repoPath = repoPathFromUrl(repoUrl);
|
||||
|
||||
if (!repoPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${repoPath}/releases?per_page=50`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const releases = (await response.json()) as ReleaseItem[];
|
||||
const candidateTags = releases
|
||||
.filter((release) => !release.draft && typeof release.tag_name === "string")
|
||||
.map((release) => (release.tag_name || "").trim())
|
||||
.filter((tag) => releaseChannelFromTag(tag) === normalizedChannel);
|
||||
|
||||
if (candidateTags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const latest = candidateTags.sort((a, b) => compareVersions(b, a))[0];
|
||||
|
||||
latestVersionCache[normalizedChannel] = {
|
||||
value: latest,
|
||||
expiresAt: now + VERSION_CACHE_TTL_MS,
|
||||
};
|
||||
|
||||
return latest;
|
||||
};
|
||||
|
||||
export const hasVersionUpdate = (
|
||||
currentVersion: string,
|
||||
latestVersion: string,
|
||||
): boolean => {
|
||||
return (
|
||||
compareVersions(normalizeTag(currentVersion), normalizeTag(latestVersion)) <
|
||||
0
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user