A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v4 213 lines 7.5 kB view raw
1import { 2 canParse, 3 greaterThan, 4 parse as parseSemver, 5 satisfies, 6 tryParseRange, 7} from "@std/semver"; 8 9/** 10 * Given the current URL segment and the latest known artifact, returns whether 11 * the user is already on the latest version. 12 * 13 * @param {string} versionOrCid - The first path segment of the current URL 14 * @param {{ cid: string, version: string } | null} lastArtifact - The latest artifact 15 * @returns {boolean} 16 * 17 * @example No artifact means always latest 18 * ```js 19 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 20 * 21 * if (!checkIsLatest("4.0.0", null)) throw new Error("no artifact should be latest"); 22 * ``` 23 * 24 * @example CID comparison 25 * ```js 26 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 27 * 28 * const artifact = { cid: "bafyabc", version: "4.0.0" }; 29 * if (!checkIsLatest("bafyabc", artifact)) throw new Error("matching CID should be latest"); 30 * if (checkIsLatest("bafyxyz", artifact)) throw new Error("different CID should not be latest"); 31 * ``` 32 * 33 * @example Exact version comparison 34 * ```js 35 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 36 * 37 * const artifact = { cid: "bafyabc", version: "4.0.0" }; 38 * if (!checkIsLatest("4.0.0", artifact)) throw new Error("matching version should be latest"); 39 * if (checkIsLatest("3.9.0", artifact)) throw new Error("older version should not be latest"); 40 * ``` 41 * 42 * @example Version range (e.g. 4.0.x) 43 * ```js 44 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 45 * 46 * const artifact = { cid: "bafyabc", version: "4.0.5" }; 47 * if (!checkIsLatest("4.0.x", artifact)) throw new Error("latest within range should be latest"); 48 * if (checkIsLatest("3.x", artifact)) throw new Error("latest outside range should not be latest"); 49 * ``` 50 * 51 * @example Caret and tilde ranges 52 * ```js 53 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 54 * 55 * const artifact = { cid: "bafyabc", version: "4.1.0" }; 56 * if (!checkIsLatest("^4.0.1", artifact)) throw new Error("^4.0.1 should match 4.1.0"); 57 * if (checkIsLatest("~4.0.1", artifact)) throw new Error("~4.0.1 should not match 4.1.0"); 58 * ``` 59 * 60 * @example Partial versions are filled in with zeros (>=4 is equivalent to >=4.0.0) 61 * ```js 62 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 63 * 64 * const artifact = { cid: "bafyabc", version: "4.1.0" }; 65 * if (!checkIsLatest(">=4", artifact)) throw new Error(">=4 should match 4.1.0"); 66 * if (checkIsLatest(">=5", artifact)) throw new Error(">=5 should not match 4.1.0"); 67 * ``` 68 * 69 * @example Non-semver, non-range slugs are always latest 70 * ```js 71 * import { checkIsLatest } from "~/common/pages/version-upgrade.js"; 72 * 73 * const artifact = { cid: "bafyabc", version: "4.0.0" }; 74 * if (!checkIsLatest("some-branch", artifact)) throw new Error("non-semver slug should be latest"); 75 * ``` 76 */ 77export function checkIsLatest(versionOrCid, lastArtifact) { 78 if (!lastArtifact) return true; 79 const usesCid = versionOrCid.startsWith("bafy"); 80 if (usesCid) return versionOrCid === lastArtifact.cid; 81 if (canParse(versionOrCid)) return versionOrCid === lastArtifact.version; 82 const versionRange = tryParseRange(versionOrCid); 83 if (versionRange) { 84 return satisfies(parseSemver(lastArtifact.version), versionRange); 85 } 86 return true; 87} 88 89/** 90 * @param {Record<string, { version: string, cid: string }>} artifacts 91 * @param {{ includePrerelease?: boolean }} [options] 92 * @returns {{ version: string, cid: string } | null} 93 * 94 * @example Returns null for an empty artifact list 95 * ```js 96 * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; 97 * 98 * if (getLatestArtifact({}) !== null) throw new Error("empty artifacts should return null"); 99 * ``` 100 * 101 * @example Returns the highest semver artifact 102 * ```js 103 * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; 104 * 105 * const artifacts = { 106 * a: { cid: "a", version: "4.0.0" }, 107 * b: { cid: "b", version: "4.1.0" }, 108 * c: { cid: "c", version: "3.9.0" }, 109 * }; 110 * if (getLatestArtifact(artifacts)?.cid !== "b") throw new Error("should return highest version"); 111 * ``` 112 * 113 * @example Ignores non-semver versions 114 * ```js 115 * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; 116 * 117 * const artifacts = { 118 * a: { cid: "a", version: "4.0.0" }, 119 * b: { cid: "b", version: "some-branch" }, 120 * }; 121 * if (getLatestArtifact(artifacts)?.cid !== "a") throw new Error("should ignore non-semver versions"); 122 * ``` 123 * 124 * @example Excludes prerelease artifacts when includePrerelease is false 125 * ```js 126 * import { getLatestArtifact } from "~/common/pages/version-upgrade.js"; 127 * 128 * const artifacts = { 129 * a: { cid: "a", version: "4.1.0" }, 130 * b: { cid: "b", version: "4.2.0-nightly.1" }, 131 * }; 132 * if (getLatestArtifact(artifacts, { includePrerelease: false })?.cid !== "a") { 133 * throw new Error("should exclude prerelease artifacts"); 134 * } 135 * if (getLatestArtifact(artifacts, { includePrerelease: true })?.cid !== "b") { 136 * throw new Error("should include prerelease artifacts when opted in"); 137 * } 138 * ``` 139 */ 140export function getLatestArtifact(artifacts, { includePrerelease = true } = {}) { 141 return Object.values(artifacts).reduce( 142 /** @param {{ version: string, cid: string } | null} max */ 143 (max, artifact) => { 144 if (!canParse(artifact.version)) return max; 145 if (!includePrerelease && parseSemver(artifact.version).prerelease?.length) return max; 146 if (!max) return artifact; 147 return greaterThan(parseSemver(artifact.version), parseSemver(max.version)) 148 ? artifact 149 : max; 150 }, 151 /** @type {{ version: string, cid: string } | null} */ (null), 152 ); 153} 154 155/** @param {Element} status */ 156function removeLoadingAnimation(status) { 157 status.querySelectorAll(".ph-spinner").forEach((icon) => { 158 icon.parentElement?.classList.add("hidden"); 159 160 setTimeout(() => { 161 icon.parentElement?.classList.remove("animate-spin"); 162 icon.classList.remove("ph-spinner"); 163 icon.classList.add("ph-arrow-fat-lines-up"); 164 }, 500); 165 }); 166} 167 168/** 169 * @param {Element} status 170 * @param {{ usesCid: boolean, isLatest: boolean }} options 171 */ 172function updateUpgradeLink(status, { usesCid, isLatest }) { 173 status.querySelectorAll(`[href="/latest/"]`).forEach((a) => { 174 if (usesCid) a.setAttribute("href", "/latest/hash/"); 175 if (!isLatest) setTimeout(() => a.classList.remove("hidden"), 750); 176 }); 177} 178 179/** 180 * Setup version upgrade (only works with `diffuse-artifacts` deployments) 181 */ 182export async function versionUpgrade() { 183 const isDiffuseDomain = document.location.hostname.endsWith("diffuse.sh"); 184 185 if (!isDiffuseDomain) { 186 document.querySelectorAll("#status a").forEach((el) => { 187 el.classList.add("hidden"); 188 }); 189 190 return; 191 } 192 193 const versionOrCid = 194 document.location.pathname.slice(1).split("/")[0]?.toLowerCase() ?? ""; 195 const usesCid = versionOrCid.startsWith("bafy"); 196 197 const { default: artifacts } = await import( 198 `${document.location.origin}/artifacts.json`, 199 { with: { type: "json" } } 200 ).catch(() => ({ default: {} })); 201 202 const currentIsStable = 203 canParse(versionOrCid) && !parseSemver(versionOrCid).prerelease?.length; 204 const lastArtifact = getLatestArtifact(artifacts, { 205 includePrerelease: !currentIsStable, 206 }); 207 const isLatest = checkIsLatest(versionOrCid, lastArtifact); 208 209 document.querySelectorAll("#status").forEach((status) => { 210 removeLoadingAnimation(status); 211 updateUpgradeLink(status, { usesCid, isLatest }); 212 }); 213}