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:
Rūdolfs Ošiņš
2026-11-10 12:00:51 +01:00
parent 1ac894481d
commit cafd882ad9
7 changed files with 95 additions and 27 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+1 -6
View File
@@ -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"
}
+20 -16
View File
@@ -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>
+1
View File
@@ -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,
+67
View File
@@ -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);
});
});