Skip to content
Open
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
57 changes: 0 additions & 57 deletions .github/workflows/update-chain-specs.yml

This file was deleted.

50 changes: 0 additions & 50 deletions apps/host/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,55 +177,6 @@ async function resolveSmoldotCommit(version: string): Promise<string> {

const SMOLDOT_COMMIT = await resolveSmoldotCommit(readSmoldotVersion());

/**
* Extract unique WSS bootnode hostnames from a chain spec JSON file.
*/
function extractBootnodeHosts(specPath: string): string[] {
try {
const spec = JSON.parse(readFileSync(specPath, "utf8")) as {
bootNodes?: string[];
};
const hosts = new Set<string>();
for (const bn of spec.bootNodes ?? []) {
if (bn.includes("/wss/") || bn.includes("/tls/ws/")) {
const match = /\/dns[46]?\/([^/]+)/.exec(bn);
if (match?.[1]) hosts.add(match[1]);
}
}
return [...hosts];
} catch {
return [];
}
}

/**
* Vite plugin that injects <link rel="preconnect"> for smoldot relay chain
* and Asset Hub bootnode hostnames.
*/
function preconnectBootnodes(): Plugin {
return {
name: "preconnect-bootnodes",
transformIndexHtml(html) {
const specDir = resolve(
import.meta.dirname,
"../../packages/resolver/src/chain-specs",
);
const hosts = [
...extractBootnodeHosts(resolve(specDir, "paseo.smol.json")),
...extractBootnodeHosts(resolve(specDir, "paseo-asset-hub.smol.json")),
];
const unique = [...new Set(hosts)];
const links = unique
.map(
(host) =>
`<link rel="preconnect" href="https://${host}" crossorigin />`,
)
.join("\n ");
return html.replace("</head>", ` ${links}\n </head>`);
},
};
}

/**
* Vite plugin that injects conditional <link rel="modulepreload"> for
* critical chunks on subdomain pages.
Expand Down Expand Up @@ -394,7 +345,6 @@ export default defineConfig({
: "/",
plugins: [
wasm(),
preconnectBootnodes(),
preloadCriticalAssets(),
previewCoepHeaders(),
copyTruapiWasmWebBundle(),
Expand Down
51 changes: 5 additions & 46 deletions apps/protocol/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,52 +665,28 @@ async function initDirectMode(): Promise<void> {
);

// Dynamic imports so users in `rpc` or `shared-worker` submode don't pay
// the smoldot / chain-specs bundle cost (D-1).
const [{ createChainProvider, isChainSupported }, resolve, smoldotMod] =
// the chain-provider bundle cost (D-1).
const [{ createChainProvider, isChainSupported }, resolve] =
await Promise.all([
import("@dotli/resolver/chains"),
import("@dotli/resolver/provider"),
import("@dotli/resolver/resolve"),
import("@dotli/resolver/smoldot"),
]);
const {
getRelayChain,
getSmoldot,
resolveDotName,
resolveExecutableManifest,
resolveOwner,
resolveRootManifest,
setResolverAssetHubProvider,
setResolverPeopleProvider,
waitForPeopleFinalized,
} = resolve;
const { terminateSmoldot, onSmoldotFatal } = smoldotMod;

// 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
// host shell.
onSmoldotFatal((message) => {
log.error("[dot.li protocol] Smoldot panic detected, signaling fatal");
if (window.parent !== window) {
window.parent.postMessage(
{
namespace: "dotli:protocol",
kind: "fatal",
message,
},
"*",
);
}
});

const engine = createEngine({
createChainProvider,
isChainSupported,
onBrokerReady: (broker) => {
// Route the resolver's Asset Hub reads AND the People warm-keep through
// the broker's shared follows (object-wire — see protocol-shared-worker
// for the rationale). A separate getSmProvider on either chain would race
// the broker's follow on the same smoldot chain and get its events
// misrouted (the broker then drops them as "unknown token").
// the broker's shared follows so they reuse the broker's single follow per
// chain instead of opening their own (see protocol-shared-worker).
setResolverAssetHubProvider(() =>
requireBrokerLocalProvider(
broker,
Expand All @@ -726,23 +702,6 @@ async function initDirectMode(): Promise<void> {
),
);
},
onInit: () => {
getSmoldot();
},
onCleanup: () => {
terminateSmoldot();
},
onWarmup: async () => {
getSmoldot();
await getRelayChain();
// Warm People in the background so legacy-account auth reads do not race
// a cold parachain warp sync. Not needed for resolution, so do not await.
void waitForPeopleFinalized().catch((err: unknown) => {
log.warn(
`[dot.li protocol] People chain warm failed (retried on demand): ${String(err)}`,
);
});
},
resolveDotName,
resolveOwner,
resolveExecutableManifest,
Expand Down
77 changes: 13 additions & 64 deletions apps/protocol/src/protocol-shared-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@

// Protocol SharedWorker.
//
// Runs smoldot directly on the SharedWorker thread using `start()` from
// `polkadot-api/smoldot` (no sub-Worker needed, because the `Worker`
// constructor is not available in SharedWorkerGlobalScope).
//
// All protocol iframes (across all tabs) connect via MessagePort.
// Smoldot persists as long as at least one tab is open.
// Runs @parity/truapi-provider's embedded smoldot light client in-thread, via
// `@dotli/resolver/provider`. No sub-Worker is spawned, because the `Worker`
// constructor is not available in SharedWorkerGlobalScope. All protocol iframes
// across every tab connect over MessagePort and share the one light client,
// which persists as long as at least one tab is open.

/// <reference lib="webworker" />
declare const self: SharedWorkerGlobalScope;
Expand All @@ -20,10 +19,11 @@ import {
setNetworkOverride,
getActiveServicesConfig,
} from "@dotli/config/network";
import { createChainProvider, isChainSupported } from "@dotli/resolver/chains";
import {
getRelayChain,
getSmoldotDirect,
createChainProvider,
isChainSupported,
} from "@dotli/resolver/provider";
import {
resolveDotName,
resolveExecutableManifest,
resolveOwner,
Expand All @@ -33,7 +33,6 @@ import {
waitForAssetHubFinalized,
waitForPeopleFinalized,
} from "@dotli/resolver/resolve";
import { onSmoldotFatal } from "@dotli/resolver/smoldot";
import { m } from "@dotli/metrics/metrics";
import * as S from "@dotli/metrics/spans";
import { initSentry, installGlobalErrorHandlers } from "@dotli/metrics/sentry";
Expand Down Expand Up @@ -118,32 +117,6 @@ if (requestedNetwork === null) {
// Placeholder broker manager until pre-sync creates the real one.
let chainBrokerManager: ReturnType<typeof createChainBrokerManager>;

// Smoldot panic broadcast. When smoldot's log callback detects a WASM
// panic, relay a `fatal` envelope to every connected port so the host
// client rejects every in-flight request immediately instead of waiting
// for a per-request timeout. `onSmoldotFatal` is idempotent and replays
// the last panic to late subscribers, so firing this once at module
// load is enough for the lifetime of the SharedWorker.
onSmoldotFatal((message) => {
swError(
`Smoldot panic detected, broadcasting fatal to ${String(ports.size)} port(s)`,
);
const fatal: ProtocolEnvelope = {
namespace: "dotli:protocol",
kind: "fatal",
message,
};
const msg: SWRelayResponse = { type: "relay-response", envelope: fatal };
for (const port of ports) {
try {
port.postMessage(msg);
// eslint-disable-next-line no-restricted-syntax -- defensive fatal broadcast: one closed port must not prevent delivery to the rest. `removePort` already cleans up ports that throw on later sends.
} catch {
/* port already disconnected, ignore on broadcast */
}
}
});

// NO retries. NO cleanup-and-retry. NO backoff. The user picked
// smoldot-shared-worker. If presync fails the actual cause is surfaced to
// every waiting port and the engine stays dead until the user reloads.
Expand All @@ -152,34 +125,10 @@ let presyncFailureMessage: string | null = null;

async function presync(): Promise<void> {
const t0 = performance.now();
m.breadcrumb("smoldot presync starting");
m.breadcrumb("presync starting");

try {
// 1. Create smoldot on the SharedWorker's own thread.
//
// `getSmoldotDirect()` is the in-thread smoldot bootstrap helper. The
// name is a polkadot-api convention meaning "run smoldot on the
// current execution context", NOT the dot.li chain backend named
// "smoldot-direct". Inside a SharedWorker the `Worker` constructor
// is unavailable, so this is the only option. The chain backend the
// user picked is still honored via the iframe's `?mode=` param.
swLog("Creating smoldot on SharedWorker thread...");
getSmoldotDirect();
m.measure(S.SMOLDOT_CREATE, performance.now() - t0);
swLog(
`Smoldot client created (${String(Math.round(performance.now() - t0))}ms)`,
);

// 2. Add relay chain
swLog("Adding relay chain...");
const relayT0 = performance.now();
await getRelayChain();
m.measure(S.SMOLDOT_RELAY_CHAIN, performance.now() - relayT0);
swLog(
`Relay chain added (${String(Math.round(performance.now() - t0))}ms)`,
);

// 3. Create the broker FIRST and route the resolver's Asset Hub reads
// Create the broker FIRST and route the resolver's Asset Hub reads
// through it as a local session, so there is one shared Asset Hub follow
// (never removed mid-read) instead of a separate resolver chain the first
// dApp connection would release — the `ChainHead disjointed` load failure.
Expand All @@ -203,7 +152,7 @@ async function presync(): Promise<void> {
),
);

// 4. Wait for Asset Hub to sync to a finalized block via the
// Wait for Asset Hub to sync to a finalized block via the
// explicit presync primitive (no more overloading `resolveDotName`
// with a sentinel label). This now syncs the broker's shared chain.
swLog("Waiting for Asset Hub to reach finalized block...");
Expand All @@ -215,7 +164,7 @@ async function presync(): Promise<void> {
m.distribution(S.SMOLDOT_PRESYNC, totalMs);
swLog(`Asset Hub synced (${String(Math.round(totalMs))}ms total)`);

// 5. Success: mark ready.
// Success: mark ready.
swLog("Pre-sync complete, engine ready");
engineReady = true;

Expand Down
18 changes: 17 additions & 1 deletion bun.lock

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

Loading