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
25 changes: 24 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Run `make help` for the list of targets.

.DEFAULT_GOAL := help
.PHONY: help setup build codegen test check check-generated clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check provider-android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-cli dev-bootstrap dev-link-check e2e-dotli e2e-cli-diagnosis e2e-signing-cli e2e-pairing-cli e2e-chat-cli e2e-cli-update headless install cli-runner cli-dist matrix explorer xcframework
.PHONY: help setup build codegen test check check-generated clean playground wasm wasm-crypto-test uniffi uniffi-kotlin android-check provider-android-check ios-build ios-run ios-chat-run ios-chat-host-playground-run ios-chat-all android-jni android-publish-local dotli-link dev dev-cli dev-bootstrap debugger dev-link-check e2e-dotli e2e-cli-diagnosis e2e-signing-cli e2e-pairing-cli e2e-chat-cli e2e-cli-update headless install cli-runner cli-dist matrix explorer xcframework

CARGO ?= cargo
TRUAPI_PKG := js/packages/truapi
Expand All @@ -25,6 +25,8 @@ DOTLI_TRUAPI_LINK := $(DOTLI_NODE_MODULES)/@parity/truapi
DOTLI_HOST_WASM_LINK := $(DOTLI_NODE_MODULES)/@parity/truapi-host
DOTLI_UI_TRUAPI_SHADOW := $(DOTLI_UI)/node_modules/@parity/truapi
DOTLI_UI_HOST_WASM_SHADOW := $(DOTLI_UI)/node_modules/@parity/truapi-host
DEBUGGER_PKG := $(JS_PACKAGES)/truapi-debugger
DEBUGGER_PORT ?= 9231
VITE_NETWORKS ?= paseo-next-v2,previewnet
export VITE_NETWORKS

Expand Down Expand Up @@ -396,6 +398,27 @@ dev: dev-bootstrap ## Start dotli host (:5173) + playground (:3000) together; op
( until curl -fsS http://localhost:3000/ >/dev/null 2>&1; do sleep 1; done; curl -fsS http://localhost:3000/diagnostics >/dev/null 2>&1 || true ) & \
wait

debugger: dev-bootstrap ## Wire debugger (:9231) + a DEV-MODE dotli host (:5173) + playground (:3000). Open http://127.0.0.1:9231
# `make dev` cannot drive the debugger: dotli ships only `build` and `preview`,
# both production builds, and the dial sits behind `import.meta.env.DEV`, which
# a production bundle replaces with `false`. The host then never dials and the
# board stays empty with no error - so build the host in dev mode here.
cd $(DOTLI)/apps/host && NODE_ENV=development VITE_APP_DEBUG=true \
VITE_TRUAPI_DEBUGGER=ws://127.0.0.1:$(DEBUGGER_PORT) bunx --bun vite build
@printf '\n Debugger: http://127.0.0.1:$(DEBUGGER_PORT)\n'
@printf ' Host: http://localhost:5173/localhost:3000\n\n'
@printf ' The dial URL is built into the host, so there is nothing to enable and it\n'
@printf ' works in any browser profile. The host logs `wire debugger: dialling ...\n'
@printf ' from the build` once it connects.\n\n'
@printf ' To aim it somewhere else instead, set the key on http://localhost:5173\n'
@printf ' (the realm that creates the host runtime); a key set by hand always wins:\n\n'
@printf ' localStorage.setItem("truapi:debugger", "ws://127.0.0.1:$(DEBUGGER_PORT)"); location.reload()\n\n'
@trap 'kill 0' EXIT; \
( cd $(DEBUGGER_PKG) && TRUAPI_DEBUGGER_PORT=$(DEBUGGER_PORT) bun run src/server.ts ) & \
( cd $(DOTLI) && bun scripts/preview-server.ts ) & \
( cd $(PLAYGROUND) && yarn dev ) & \
wait

e2e-dotli: ## Fully automated dotli + playground diagnosis e2e using the local signing-host CLI.
@$(MAKE) dev-bootstrap
cargo build -p truapi-host-cli
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,28 @@ host owns where the bytes live. The crate stores nothing itself: a host implemen
`StorageClient` over storage it already owns, on web and native alike, so it keeps
control of quota and of whether the bytes are backed up or encrypted.

### Wire debugger

[`@parity/truapi-debugger`](js/packages/truapi-debugger) is the consumer for the
payload-blind frame tap in `truapi-server`. The core streams raw SCALE frames out
of two choke points; the debugger correlates them into per-operation traces,
decodes envelopes and values behind a `TRUAPI_WIRE_SCHEMA_HASH` match, and renders
them through one of two mounts:

- `startDebugServer(...)` is a standalone Bun WS+HTTP server on `127.0.0.1:9231`
that hosts dial into, so frames from any host reach one inspector.
- `createInAppDebugger(...)` mounts the same engine inside the host page, with no
server and no dial.

All decoding lives in this package; `@parity/truapi` has no debug seam. Its
[README](js/packages/truapi-debugger/README.md) carries the endpoint list and the
per-host enablement recipe.

`make debugger` brings up the inspector on `:9231` alongside a dot.li host and the
playground. It builds the host with `NODE_ENV=development` on purpose: the dial
sits behind `import.meta.env.DEV`, which a production bundle replaces with `false`,
so `make dev` leaves the board empty with no error.

## How it works

1. The protocol is defined as Rust traits in [`rust/crates/truapi/`](rust/crates/truapi/), with each method tagged `#[wire(id = N)]` for a stable byte-level dispatch table. Every method's doc comment must carry a ` ```ts ` example, which codegen extracts into the playground's EXAMPLE tab; the build fails if any method is missing one.
Expand Down
6 changes: 5 additions & 1 deletion docs/design/wire-observability-debug-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,11 @@ installed, which §9's enablement rules govern.

- The web host reads its debugger URL behind a build-time DEV condition, so a
production bundle returns no URL and a stray `localStorage` key cannot turn the
tap on. With no URL the host installs no emit callback, so the core installs no
tap on. The URL has two sources, both inside that condition: a `localStorage`
key, and a value the dev build carries. A key set by hand **MUST** win, so a
build's value is a default rather than an override, and the host **MUST** report
which source it used - an ignored key is otherwise indistinguishable from a key
that was never read. With no URL the host installs no emit callback, so the core installs no
sink. The condition **MUST** be the bare token the bundler substitutes
(`import.meta.env.DEV`) — no alias, no optional chaining. A bundler replaces
that exact token and nothing else; an aliased read survives into the bundle,
Expand Down
7 changes: 7 additions & 0 deletions js/packages/truapi-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ no tap:
localStorage.setItem("truapi:debugger", "ws://127.0.0.1:9231");
```

A dev build can carry the URL instead, which is what a local stack does:
build the host with `VITE_TRUAPI_DEBUGGER=ws://127.0.0.1:9231` and every
browser profile that opens it dials without a key. A key set by hand always
wins over the build's value, so it stays an override. `localStorage` is
per-origin and per-profile, which is the reason a build-time default exists
at all: a key cannot be arranged from outside the browser.

Run the debugger at the other end (`@parity/truapi-debugger`, `npm run serve`,
`127.0.0.1:9231`). On the next runtime boot the worker dials that URL and (via
the Rust core's `DebugSink` tap) sends each frame as `{ channelId, dir, frame }`.
Expand Down
48 changes: 45 additions & 3 deletions js/packages/truapi-host/src/web/create-worker-host-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,38 @@ type DebuggerEnablement = {
| "enabled"
| "production-build"
| "production-build-switch-set"
| "enabled-from-build"
| "no-key"
| "no-storage";
};

/**
* Dial URL a dev build was given, when it was given one.
*
* Lets a local stack point the tap at its own debugger with no per-browser setup:
* `localStorage` is per-origin AND per-profile, so a key is the one piece of this
* that cannot be arranged from outside the browser. A build-time value can, and it
* then holds for every profile that opens that build.
*
* Same literal-token rule as the `DEV` read below: a bundler replaces the exact
* `import.meta.env.VITE_TRUAPI_DEBUGGER` expression, so it must not be aliased or
* optionally chained. The try/catch covers realms with no `import.meta.env` at all
* (tsc output under Node, unit tests), where the access throws.
*/
function buildTimeDebuggerUrl(): string | null {
let raw: unknown;
try {
raw = (
import.meta as unknown as { env: { VITE_TRUAPI_DEBUGGER?: unknown } }
).env.VITE_TRUAPI_DEBUGGER;
} catch {
return null;
}
if (typeof raw !== "string") return null;
const url = raw.trim();
return url === "" ? null : url;
}

function readPersistedDebuggerUrl(): DebuggerEnablement {
// Hard dev-only gate, not a convention: bundlers (Vite) replace
// `import.meta.env.DEV` with a boolean literal, so in a PRODUCTION build this
Expand Down Expand Up @@ -284,10 +312,14 @@ function readPersistedDebuggerUrl(): DebuggerEnablement {
};
}
const storage = globalThis.localStorage;
const url = storage?.getItem(DEV_DEBUGGER_URL_KEY) ?? null;
// A key set by hand wins, so a developer can always aim a build somewhere else
// without rebuilding it. The build-time value is the default, not an override.
if (url !== null && url !== "") return { url, reason: "enabled" };
const fromBuild = buildTimeDebuggerUrl();
if (fromBuild !== null) return { url: fromBuild, reason: "enabled-from-build" };
if (storage === undefined) return { url: null, reason: "no-storage" };
const url = storage.getItem(DEV_DEBUGGER_URL_KEY);
if (url === null || url === "") return { url: null, reason: "no-key" };
return { url, reason: "enabled" };
return { url: null, reason: "no-key" };
}

/**
Expand Down Expand Up @@ -323,6 +355,16 @@ function reportDebuggerEnablement(e: DebuggerEnablement): void {
console.info(`[truapi] wire debugger: dialling ${e.url} (origin ${origin})`);
return;
}
if (e.reason === "enabled-from-build") {
// Say where the URL came from. A developer who never set a key needs to know
// the build chose one, and a developer whose key was ignored needs to know it
// was not: a key always wins, so seeing this line means no key was set.
console.info(
`[truapi] wire debugger: dialling ${e.url} from the build (origin ${origin}); ` +
`set "${DEV_DEBUGGER_URL_KEY}" here to override`,
);
return;
}
const why =
e.reason === "no-storage"
? "no localStorage in this realm"
Expand Down