Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9fa6435
feat: wire smoldot lifecycle events to the loading bar
BigTava Aug 5, 2026
43048d7
Merge remote-tracking branch 'origin/main' into feat/smoldot-lifecycle
BigTava Aug 5, 2026
e4fd7df
Fix formatting
BigTava Aug 5, 2026
57ab0e1
Document the maxLogLevel coupling, unit-test the lifecycle parser, an…
BigTava Aug 5, 2026
3421ed9
Show a live peer count and honest stall copy on the loading screen vi…
BigTava Aug 5, 2026
ec1d17d
Intercept lifecycle and health traffic in the chain tap instead of th…
BigTava Aug 5, 2026
bdbdeea
Merge the lifecycle and health channels into one chain-sync stream an…
BigTava Aug 5, 2026
50b1202
Bring the loading screen text to WCAG AA, expose the peer count to sc…
BigTava Aug 5, 2026
187f56e
Narrate the Asset Hub sync step with the relay peer count, warp progr…
BigTava Aug 6, 2026
a9d4270
Rebuild the loading screen around a bar that only explains itself aft…
BigTava Aug 6, 2026
055dc9a
Restructure the network panel around a live status line and a per-cha…
BigTava Aug 6, 2026
08ed85c
Add the chain-sync subscription to the protocol client mock the topba…
BigTava Aug 7, 2026
8291aef
Narrate the loading screen in plain language and keep the bar moving …
BigTava Aug 7, 2026
3d77a23
Reword the loading messages to name the domain and drop the chain jargon
BigTava Aug 7, 2026
1467f12
Turn the logo into the progress indicator and retire the loading bar
BigTava Aug 7, 2026
045b735
Fill the logo left to right and retype the status line character by c…
BigTava Aug 7, 2026
3363217
Slow and ease the status line turnover, match the network panel paddi…
BigTava Aug 7, 2026
c9f74a2
Spell the chain AssetHub in the light client status row
BigTava Aug 7, 2026
76b39b6
Hold the logo fill to the download's real percentage instead of the c…
BigTava Aug 7, 2026
a77debf
chore(deps): bump smoldot to 3.4.1-dev-20260811.0
BigTava Aug 11, 2026
d633d08
Measure the whole load for Completed and Speed, and show relay peers
BigTava Aug 11, 2026
f0ee0de
Scope progress signals to the band that owns them and stop the networ…
BigTava Aug 11, 2026
955568e
Restore the progress bar, drop the Completed readout, and slow the se…
BigTava Aug 11, 2026
6b92c38
Remove the gateway escape button from the loading screen
BigTava Aug 11, 2026
0441f33
Show only how long ago each block landed and reword the connected status
BigTava Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
733 changes: 480 additions & 253 deletions apps/host/index.html

Large diffs are not rendered by default.

342 changes: 288 additions & 54 deletions apps/host/src/main.ts

Large diffs are not rendered by default.

67 changes: 2 additions & 65 deletions apps/host/tests/functional/loading.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,30 +131,6 @@ const successfulResolveResponse = (cid: string): string => `
});
`;

/** Rescale any setTimeout call whose delay matches `fromMs` down to `toMs`. */
async function shrinkTimeout(
page: Page,
fromMs: number,
toMs: number,
): Promise<void> {
await page.addInitScript(
([from, to]) => {
const orig = window.setTimeout.bind(window);
window.setTimeout = ((
handler: TimerHandler,
ms?: number,
...rest: unknown[]
) =>
orig(
handler,
ms === from ? to : ms,
...rest,
)) as typeof window.setTimeout;
},
[fromMs, toMs],
);
}

test("As a user using smoldot directly, when the light client panics mid-resolution, I see the appropriate error and can switch backend", async ({
page,
}) => {
Expand Down Expand Up @@ -265,12 +241,11 @@ test("As a user using smoldot in shared worker, when the worker dies silently, I
);
});

test("As a user using smoldot directly, when loading is slow (>10s) I see a one-click gateway escape, and if it times out (>45s) I see the appropriate error and can switch backend", async ({
test("As a user using smoldot directly, when the resolution times out I see the appropriate error and can switch backend", async ({
page,
}) => {
// Given
await setBackend(page, "smoldot-direct");
await shrinkTimeout(page, 10_000, 500);
await mockProtocolIframe(
page,
errorResolveResponse(
Expand All @@ -283,10 +258,6 @@ test("As a user using smoldot directly, when loading is slow (>10s) I see a one-
await page.goto(HOST_URL, { waitUntil: "domcontentloaded" });

// Then
await expect(page.locator(".loading-gateway-btn")).toContainText(
"Use Trusted Provider",
{ timeout: 5_000 },
);
await expect(page.locator(".error-page-title")).toHaveText(
"Domain can't be reached",
{ timeout: 10_000 },
Expand All @@ -302,12 +273,11 @@ test("As a user using smoldot directly, when loading is slow (>10s) I see a one-
);
});

test("As a user using smoldot in shared worker, when loading is slow (>10s) I see a one-click gateway escape, and if it times out (>45s) I see the appropriate error and can switch backend", async ({
test("As a user using smoldot in shared worker, when the resolution times out I see the appropriate error and can switch backend", async ({
page,
}) => {
// Given
await setBackend(page, "smoldot-shared-worker");
await shrinkTimeout(page, 10_000, 500);
await mockProtocolIframe(
page,
errorResolveResponse(
Expand All @@ -320,10 +290,6 @@ test("As a user using smoldot in shared worker, when loading is slow (>10s) I se
await page.goto(HOST_URL, { waitUntil: "domcontentloaded" });

// Then
await expect(page.locator(".loading-gateway-btn")).toContainText(
"Use Trusted Provider",
{ timeout: 5_000 },
);
await expect(page.locator(".error-page-title")).toHaveText(
"Domain can't be reached",
{ timeout: 10_000 },
Expand All @@ -339,35 +305,6 @@ test("As a user using smoldot in shared worker, when loading is slow (>10s) I se
);
});

test("As a user using smoldot directly, when I click the gateway escape, the backend flips to rpc-gateway and the page reloads", async ({
page,
}) => {
// Given
await setBackend(page, "smoldot-direct");
await shrinkTimeout(page, 10_000, 500);
await mockProtocolIframe(
page,
errorResolveResponse("never resolves in test window", 30_000),
);

// When
await page.goto(HOST_URL, { waitUntil: "domcontentloaded" });
const gatewayBtn = page.locator(".loading-gateway-btn");
await expect(gatewayBtn).toContainText("Use Trusted Provider", {
timeout: 5_000,
});
await Promise.all([
page.waitForLoadState("domcontentloaded"),
gatewayBtn.click(),
]);

// Then
const backend = await page.evaluate(() =>
localStorage.getItem("dotli:chain-backend"),
);
expect(backend).toBe("rpc-gateway");
});

test("As a user, when the app chunks fail to load mid-session, I see the appropriate error with a reload button", async ({
page,
}) => {
Expand Down
67 changes: 67 additions & 0 deletions apps/host/tests/functional/resolution.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,71 @@ test.describe("Resolution across chain backends", () => {
}
});
}

test(`As a user opening ${DOMAIN}.dot, I am shown how many peers the light client found`, async ({
browser,
}) => {
// Given
const { context, page } = await setupTest(browser, {
backend: "smoldot-direct",
});

try {
// Record the counts as they reach the shell. The rendered figure can
// change faster than a poll can catch, so the envelope is the reliable
// signal and the visible readout is accepted as an alternative.
await page.addInitScript(() => {
const seen: unknown[] = [];
(
window as unknown as { __dotliPeerCounts: unknown[] }
).__dotliPeerCounts = seen;
window.addEventListener("message", (event: MessageEvent) => {
const data = event.data as {
namespace?: string;
kind?: string;
syncKind?: string;
peers?: number;
} | null;
if (
data !== null &&
typeof data === "object" &&
data.namespace === "dotli:protocol" &&
data.kind === "chain-sync" &&
data.syncKind === "peers" &&
typeof data.peers === "number"
) {
seen.push(data);
}
});
});

// When
await page.goto(BASE_URL, { waitUntil: "commit" });

// Then
const sawPeers = page.waitForFunction(
() => {
const seen = (window as unknown as { __dotliPeerCounts?: unknown[] })
.__dotliPeerCounts;
if (seen !== undefined && seen.length > 0) {
return true;
}
return ["relay", "assethub", "bulletin"].some((chain) =>
/[1-9]/.test(
document.getElementById(`metric-peers-${chain}`)?.textContent ??
"",
),
);
},
undefined,
{ timeout: TIMEOUT_MS },
);
await Promise.all([
sawPeers,
waitForResolutionOutcome(page, TIMEOUT_MS, "smoldot-direct"),
]);
} finally {
await context.close();
}
});
});
69 changes: 68 additions & 1 deletion apps/protocol/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ import {
installGlobalErrorHandlers,
captureException,
} from "@dotli/metrics/sentry";
import {
chainBytesReceived,
installByteMeter,
} from "@dotli/resolver/byte-meter";

// Before anything opens a socket. smoldot's transports are the bulk of a
// cold load's traffic and are invisible to resource timing, so the loading
// screen's speed readout has no other source for them.
installByteMeter();

// Do NOT silently reload on chunk preload failure. The protocol iframe is
// hidden and has no UI of its own, so it surfaces the failure to the parent
Expand Down Expand Up @@ -683,7 +692,21 @@ async function initDirectMode(): Promise<void> {
setResolverPeopleProvider,
waitForPeopleFinalized,
} = resolve;
const { terminateSmoldot, onSmoldotFatal } = smoldotMod;
const { terminateSmoldot, onSmoldotFatal, onChainSync } = smoldotMod;

// Sync reporting is only worth its cost when a loading UI can observe it.
// Direct mode is that case and the SharedWorker never enables it. The
// host moves the bar on the relay and the Asset Hub, and shows a peer
// count for the Asset Hub alone.
smoldotMod.enableSyncReporting({
// All three chains the load waits on, in the order it waits on them.
// The relay warps, the Asset Hub bootstraps on top of it, and Bulletin
// serves the content over bitswap. Bulletin is not even created until
// after the content phase begins, and takes roughly another second and
// a half to find a peer, which used to be silent.
milestones: ["relay", "asset-hub", "bulletin"],
peerCounts: ["relay", "asset-hub", "bulletin"],
});

// On a smoldot panic, broadcast a fatal envelope to the parent. Direct
// mode has no SharedWorker in the loop, so we post straight up to the
Expand All @@ -702,6 +725,50 @@ async function initDirectMode(): Promise<void> {
}
});

// Forward what the chains report about their sync to the host shell, so
// the loading screen moves on real signals instead of log-scraped prose.
// This iframe owns the smoldot instance. The host has no handle on it.
onChainSync((event) => {
if (window.parent === window) {
return;
}
const { chain, kind, ...rest } = event;
window.parent.postMessage(
{
namespace: "dotli:protocol",
kind: "chain-sync",
chain,
syncKind: kind,
...rest,
},
"*",
);
});

// Feed the host's speed readout. Cumulative totals on a fixed tick rather
// than a rate, so the host owns the averaging and a dropped message just
// widens one window.
if (window.parent !== window) {
const postBytes = (): void => {
window.parent.postMessage(
{
namespace: "dotli:protocol",
kind: "net-bytes",
received: chainBytesReceived(),
},
"*",
);
};
// Send a baseline straight away. A rate needs two readings, so waiting a
// full tick for the first one delayed the whole readout by 500ms on top
// of the time this iframe took to boot.
postBytes();
const reportBytes = setInterval(postBytes, 500);
window.addEventListener("pagehide", () => {
clearInterval(reportBytes);
});
}

const engine = createEngine({
createChainProvider,
isChainSupported,
Expand Down
8 changes: 5 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
"packages/*"
],
"dependencies": {
"smoldot": "3.3.2"
"smoldot": "3.4.1-dev-20260811.0"
},
"overrides": {
"@parity/truapi": "0.7.0",
"fast-uri": "3.1.5",
"smoldot": "3.3.2",
"smoldot": "3.4.1-dev-20260811.0",
"esbuild": "^0.28.1",
"brace-expansion": "^5.0.9",
"postcss": "^8.5.24"
Expand Down
7 changes: 0 additions & 7 deletions packages/metrics/src/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,6 @@ export const AUTH_SESSION_RESTORE = "auth.session_restore";
/** WASM module load time (captured via PerformanceObserver) */
export const WASM_LOAD = "wasm.load";

/**
* User clicked the "Use gateway instead" escape hatch on the loading
* screen. Tagged with `from_backend` so we can see which verified path
* (smoldot-direct vs smoldot-shared-worker) the user bailed out of.
*/
export const GATEWAY_ESCAPE = "loading.gateway_escape";

/**
* Shared-storage request rejected before it could touch `localStorage`:
* bad siteId, malformed key, disallowed origin, or unrecognised value
Expand Down
Loading
Loading