Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 29 additions & 42 deletions apps/host/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
advancePhase,
stopStatusTick,
listenForSandboxStatus,
showGatewayEscape,
} from "@dotli/ui/ui";
import type { LoadingPhase } from "@dotli/ui/ui";
import { initTopBar, wipeOriginState } from "@dotli/ui/topbar";
Expand Down Expand Up @@ -1257,48 +1256,36 @@ async function main(): Promise<void> {
log.warn(
`[dot.li resolve] path=smoldot (trustless light-client) (${elapsed(T0)})`,
);
// After 10s of slow loading on the verified path, surface a one-click
// escape to the gateway backend. The user trades the light-client
// verification badge for a faster, trust-based load.
const cancelGatewayEscape = showGatewayEscape(() => {
m.count(S.GATEWAY_ESCAPE, { from_backend: chainBackend });
switchBackendAndReload("rpc-gateway");
});

try {
const { statusToPhase } = await import("@dotli/resolver/resolve");
const onResolveProgress = (msg: string): void => {
// Progress events arrive as opaque strings across the iframe
// boundary. The resolver package owns the authoritative
// mapping from status text to ResolvePhase, so we defer to it
// instead of maintaining a parallel regex here.
const phase = statusToPhase(msg);
if (phase === "relay-chain-adding") {
advancePhase(1);
} else if (
// `asset-hub-connecting` is ~0ms (just createClient), so it shares
// the Syncing band rather than getting a slice that makes the bar
// jump for no work.
phase === "asset-hub-connecting" ||
phase === "asset-hub-syncing" ||
phase === "asset-hub-ready"
) {
advancePhase(2);
} else if (phase === "resolving-content") {
advancePhase(3);
}
emitPhase(msg, phase ?? "progress");
trackStatus(msg);
};
cid = await resolveDotNameRemote(`app.${label}`, onResolveProgress);
if (cid === null) {
cid = await resolveDotNameRemote(label, onResolveProgress);
log.warn(
`[dot.li resolve] fallback ${withActiveTld(label)} contenthash -> ${cid ?? "null"}`,
);
const { statusToPhase } = await import("@dotli/resolver/resolve");
const onResolveProgress = (msg: string): void => {
// Progress events arrive as opaque strings across the iframe
// boundary. The resolver package owns the authoritative
// mapping from status text to ResolvePhase, so we defer to it
// instead of maintaining a parallel regex here.
const phase = statusToPhase(msg);
if (phase === "relay-chain-adding") {
advancePhase(1);
} else if (
// `asset-hub-connecting` is ~0ms (just createClient), so it shares
// the Syncing band rather than getting a slice that makes the bar
// jump for no work.
phase === "asset-hub-connecting" ||
phase === "asset-hub-syncing" ||
phase === "asset-hub-ready"
) {
advancePhase(2);
} else if (phase === "resolving-content") {
advancePhase(3);
}
} finally {
cancelGatewayEscape();
emitPhase(msg, phase ?? "progress");
trackStatus(msg);
};
cid = await resolveDotNameRemote(`app.${label}`, onResolveProgress);
if (cid === null) {
cid = await resolveDotNameRemote(label, onResolveProgress);
log.warn(
`[dot.li resolve] fallback ${withActiveTld(label)} contenthash -> ${cid ?? "null"}`,
);
}
} else {
log.warn(
Expand Down
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 @@ -133,30 +133,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 @@ -267,12 +243,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 sync times out (>45s) 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 @@ -285,10 +260,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 @@ -304,12 +275,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 sync times out (>45s) 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 @@ -322,10 +292,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 @@ -341,35 +307,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
3 changes: 2 additions & 1 deletion bun.lock

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"esbuild": "^0.28.1",
"brace-expansion": "^5.0.9",
"postcss": "^8.5.24",
"nanoid": "^3.3.18"
"nanoid": "^3.3.18",
"deepmerge-ts": "^8.0.1"
Comment thread
justraman marked this conversation as resolved.
}
}
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
3 changes: 2 additions & 1 deletion packages/resolver/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export class ContenthashDecodeError extends ResolverError {
*
* Smoldot started and added the relay chain, but the peer set never produced
* a finalized parachain block in time. Surfaced as a fatal so the host can
* offer the gateway escape rather than sit on the loading screen.
* show the error page with its trusted-provider retry rather than sit on the
* loading screen.
*/
export class NetworkSyncTimeoutError extends ResolverError {
override readonly name = "NetworkSyncTimeoutError" as const;
Expand Down
55 changes: 0 additions & 55 deletions packages/ui/src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -200,61 +200,6 @@ body {
display: block;
}

.loading-gateway-btn {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 20px auto 0;
padding: 10px 16px 10px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.92);
font-family: inherit;
cursor: pointer;
line-height: 1.2;
transition:
border-color 0.2s ease,
background 0.2s ease,
color 0.2s ease,
transform 0.15s ease;
}
.loading-gateway-btn:hover {
border-color: rgba(255, 255, 255, 0.32);
background: rgba(255, 255, 255, 0.07);
color: #fff;
}
.loading-gateway-btn:active {
transform: translateY(1px);
}
.loading-gateway-btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 7px;
background: rgba(255, 255, 255, 0.06);
color: #fafafa;
flex-shrink: 0;
}
.loading-gateway-btn-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1px;
}
.loading-gateway-btn-label {
font-size: 12.5px;
font-weight: 600;
letter-spacing: -0.01em;
}
.loading-gateway-btn-sub {
font-size: 10.5px;
color: rgba(255, 255, 255, 0.5);
letter-spacing: -0.005em;
}

.accent {
color: #fafafa;
font-weight: 600;
Expand Down
17 changes: 0 additions & 17 deletions packages/ui/src/styles/themes.css
Original file line number Diff line number Diff line change
Expand Up @@ -216,23 +216,6 @@
[data-theme="light"] .loading-hint.visible {
opacity: 0.9;
}
[data-theme="light"] .loading-gateway-btn {
border-color: rgba(0, 0, 0, 0.16);
background: rgba(0, 0, 0, 0.03);
color: rgba(0, 0, 0, 0.85);
}
[data-theme="light"] .loading-gateway-btn:hover {
border-color: rgba(0, 0, 0, 0.32);
background: rgba(0, 0, 0, 0.06);
color: #111;
}
[data-theme="light"] .loading-gateway-btn-icon {
background: rgba(0, 0, 0, 0.06);
color: #0a0a0a;
}
[data-theme="light"] .loading-gateway-btn-sub {
color: rgba(0, 0, 0, 0.55);
}
[data-theme="light"] .loading-progress-bar {
background: rgba(0, 0, 0, 0.06);
}
Expand Down
Loading
Loading