mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 11:20:36 +08:00
explorer: Use oak as the default seed
oak.radicle.garden balances between its seed nodes at the network level, so we don't need to spread users across iris and rosa anymore. A picked seed that's neither a default nor bookmarked gets cleared, which moves users who picked iris or rosa to the new default.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
"RUSTSEC-2023-0071", # https://radicle.network/nodes/iris.radicle.network/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/issues/bd8af7a5e3fd02c0963d7550a5cc2cab00950775
|
||||
"RUSTSEC-2023-0071", # https://radicle.network/nodes/oak.radicle.garden/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5/issues/bd8af7a5e3fd02c0963d7550a5cc2cab00950775
|
||||
# atomic-polyfill is unmaintained. It reaches the lockfile through
|
||||
# radicle-artifact -> postcard -> heapless 0.7, which only depends on it for
|
||||
# AVR, RISC-V, thumbv6m and Xtensa bare-metal targets, so it is never
|
||||
|
||||
+4
-3
@@ -103,7 +103,8 @@ the configuration from a pre-defined location (`/config.json`) on the server.
|
||||
|
||||
The config file must be served as static content and must be publicly
|
||||
accessible. The structure of the runtime `config.json` must match the shape of
|
||||
the application's base configuration defined in `config/default.json`.
|
||||
the application's base configuration defined in `config/default.json`. Arrays
|
||||
replace the built-in ones, and `preferredSeeds` must list at least one seed.
|
||||
|
||||
### Homepage
|
||||
|
||||
@@ -222,13 +223,13 @@ The UI is distributed under the terms of GPLv3. See [LICENSE][lic] for details.
|
||||
[con]: ../CONTRIBUTING.md
|
||||
[def]: ./config/default.json
|
||||
[iss]:
|
||||
https://radicle.network/nodes/iris.radicle.network/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5/issues
|
||||
https://radicle.network/nodes/oak.radicle.garden/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5/issues
|
||||
[lic]: ./LICENSE
|
||||
[nco]: https://github.com/node-config/node-config/wiki/Configuration-Files
|
||||
[nod]: https://nodejs.org
|
||||
[npm]: https://www.npmjs.com
|
||||
[pat]:
|
||||
https://radicle.network/nodes/iris.radicle.network/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5/patches
|
||||
https://radicle.network/nodes/oak.radicle.garden/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5/patches
|
||||
[rad]: https://radicle.dev
|
||||
[try]: https://caddyserver.com/docs/caddyfile/patterns#single-page-apps-spas
|
||||
[zul]: https://radicle.zulipchat.com/#narrow/stream/369278-web
|
||||
|
||||
@@ -12,12 +12,7 @@
|
||||
"supportWebsite": "https://radicle.zulipchat.com",
|
||||
"preferredSeeds": [
|
||||
{
|
||||
"hostname": "rosa.radicle.network",
|
||||
"port": 443,
|
||||
"scheme": "https"
|
||||
},
|
||||
{
|
||||
"hostname": "iris.radicle.network",
|
||||
"hostname": "oak.radicle.garden",
|
||||
"port": 443,
|
||||
"scheme": "https"
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@ const seedSchema = z.object({
|
||||
scheme: z.string(),
|
||||
});
|
||||
|
||||
const HARDCODED_FALLBACK: BaseUrl = {
|
||||
scheme: "https",
|
||||
hostname: "iris.radicle.network",
|
||||
port: 443,
|
||||
};
|
||||
|
||||
// The seed the user explicitly picked, or `undefined` when they never did.
|
||||
// Read `searchSeed` instead of this unless you need to distinguish an explicit
|
||||
// pick from a resolved fallback.
|
||||
@@ -49,6 +43,19 @@ export const bookmarkedSeeds = storedWritable<BaseUrl[]>(
|
||||
!window.localStorage,
|
||||
);
|
||||
|
||||
function dropRetiredDefaultSeed() {
|
||||
const pinned = get(explicitSeed);
|
||||
if (
|
||||
pinned &&
|
||||
!config.preferredSeeds.some(s => isEqual(s, pinned)) &&
|
||||
!get(bookmarkedSeeds).some(s => isEqual(s, pinned))
|
||||
) {
|
||||
explicitSeed.clear();
|
||||
}
|
||||
}
|
||||
|
||||
dropRetiredDefaultSeed();
|
||||
|
||||
// Session-scoped set of seeds the app tried and failed to reach. Used to
|
||||
// surface a warning icon in the seed picker.
|
||||
export const failedSeeds = writable<BaseUrl[]>([]);
|
||||
@@ -67,10 +74,8 @@ export function removeBookmark(seed: BaseUrl) {
|
||||
bookmarkedSeeds.update(previous => previous.filter(x => !isEqual(x, seed)));
|
||||
if (isEqual(get(explicitSeed), seed)) {
|
||||
// Clear the explicit pick so `determineSeed()` falls back through its
|
||||
// normal resolution path (bucket-pick a preferred seed, or the hardcoded
|
||||
// fallback). Setting it to the first remaining bookmark or preferred seed
|
||||
// would orphan the store when neither exists, and would also bypass the
|
||||
// bucket-based load balancing.
|
||||
// normal resolution path (bucket-pick a preferred seed). Setting it to the
|
||||
// first remaining bookmark would bypass the bucket-based load balancing.
|
||||
explicitSeed.set(undefined);
|
||||
}
|
||||
}
|
||||
@@ -94,18 +99,17 @@ function getOrCreateBucketRandom(): number {
|
||||
// bucket number against the current preferred-seeds list. This keeps a
|
||||
// user on the same seed across visits when the list is unchanged, but
|
||||
// probabilistically rebalances them onto new seeds when the list grows.
|
||||
// 3. Fallback to a hardcoded seed if nothing is configured.
|
||||
// The config must list at least one preferred seed.
|
||||
export function determineSeed(): BaseUrl {
|
||||
const explicit = get(explicitSeed);
|
||||
if (explicit) return explicit;
|
||||
|
||||
const pool = config.preferredSeeds;
|
||||
if (pool.length > 0) {
|
||||
const bucket = getOrCreateBucketRandom();
|
||||
return pool[bucket % pool.length];
|
||||
if (pool.length === 0) {
|
||||
throw new Error("Config must list at least one seed in `preferredSeeds`");
|
||||
}
|
||||
|
||||
return HARDCODED_FALLBACK;
|
||||
const bucket = getOrCreateBucketRandom();
|
||||
return pool[bucket % pool.length];
|
||||
}
|
||||
|
||||
// The seed that searches and the explore listing run against. Derived so
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
{#if activeTab === "cli"}
|
||||
<label for="download-command">
|
||||
Use the <ExternalLink
|
||||
href="https://radicle.network/nodes/iris.radicle.network/rad:z4VYyJ9KuwMNkXGQnmKuGPGKw3inv">
|
||||
href="https://radicle.network/nodes/oak.radicle.garden/rad:z4VYyJ9KuwMNkXGQnmKuGPGKw3inv">
|
||||
Radicle Artifact CLI
|
||||
</ExternalLink> to download and verify this artifact.
|
||||
</label>
|
||||
|
||||
@@ -35,6 +35,7 @@ test("explore falls back to a healthy seed when the primary is unreachable", asy
|
||||
// Make an unreachable seed the primary, then fail its requests instantly.
|
||||
await page.addInitScript(seed => {
|
||||
window.localStorage.setItem("selectedSeed", JSON.stringify(seed));
|
||||
window.localStorage.setItem("bookmarkedSeeds", JSON.stringify([seed]));
|
||||
}, unreachableSeed);
|
||||
await page.route(
|
||||
url => url.hostname === unreachableSeed.hostname,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
const { oak, iris, custom } = vi.hoisted(() => ({
|
||||
oak: { hostname: "oak.radicle.garden", port: 443, scheme: "https" },
|
||||
iris: { hostname: "iris.radicle.network", port: 443, scheme: "https" },
|
||||
custom: { hostname: "seed.example.com", port: 443, scheme: "https" },
|
||||
}));
|
||||
|
||||
vi.mock("@app/lib/config", () => ({
|
||||
default: { preferredSeeds: [oak] },
|
||||
}));
|
||||
|
||||
async function loadSeedSelector() {
|
||||
vi.resetModules();
|
||||
return import("@app/views/nodes/SeedSelector");
|
||||
}
|
||||
|
||||
describe("dropping a retired default seed on load", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
test("clears a picked seed that is no longer a default", async () => {
|
||||
localStorage.setItem("selectedSeed", JSON.stringify(iris));
|
||||
|
||||
const { explicitSeed, determineSeed } = await loadSeedSelector();
|
||||
|
||||
expect(get(explicitSeed)).toBeUndefined();
|
||||
expect(localStorage.getItem("selectedSeed")).toBeNull();
|
||||
expect(determineSeed()).toEqual(oak);
|
||||
});
|
||||
|
||||
test("keeps a picked seed that is still a default", async () => {
|
||||
localStorage.setItem("selectedSeed", JSON.stringify(oak));
|
||||
|
||||
const { explicitSeed } = await loadSeedSelector();
|
||||
|
||||
expect(get(explicitSeed)).toEqual(oak);
|
||||
});
|
||||
|
||||
test("keeps a picked seed that is bookmarked", async () => {
|
||||
localStorage.setItem("selectedSeed", JSON.stringify(custom));
|
||||
localStorage.setItem("bookmarkedSeeds", JSON.stringify([custom]));
|
||||
|
||||
const { explicitSeed, determineSeed } = await loadSeedSelector();
|
||||
|
||||
expect(get(explicitSeed)).toEqual(custom);
|
||||
expect(determineSeed()).toEqual(custom);
|
||||
});
|
||||
|
||||
test("keeps a bookmarked former default", async () => {
|
||||
localStorage.setItem("selectedSeed", JSON.stringify(iris));
|
||||
localStorage.setItem("bookmarkedSeeds", JSON.stringify([iris]));
|
||||
|
||||
const { explicitSeed } = await loadSeedSelector();
|
||||
|
||||
expect(get(explicitSeed)).toEqual(iris);
|
||||
});
|
||||
|
||||
test("leaves users without a picked seed on the default", async () => {
|
||||
const { explicitSeed, determineSeed } = await loadSeedSelector();
|
||||
|
||||
expect(get(explicitSeed)).toBeUndefined();
|
||||
expect(determineSeed()).toEqual(oak);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user