Skip to content

Commit 4ec2f60

Browse files
committed
fixes
1 parent 6bf54f0 commit 4ec2f60

25 files changed

Lines changed: 384 additions & 729 deletions

playground/CLAUDE.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,13 @@ The Diagnosis screen emits a per-host markdown report via "Copy report". Aggrega
3636
| --- | --- |
3737
| `src/lib/services.ts` | Re-exports `services` from `@parity/truapi/playground/services`, which the Rust codegen produces from rustdoc `ts` examples. Read-only. |
3838
| `src/lib/transport.ts` | Singleton `Provider`/`Transport`/`TrUApiClient` over iframe postMessage or webview MessagePort. Owns the handshake and connection status. |
39-
| `src/lib/example-runner.ts` | Transpiles each rustdoc `ts` example via sucrase, runs it inside an `AsyncFunction` with `truapi`, `console`, and rxjs as ambient bindings. A tracking Proxy auto-unsubscribes inner `.subscribe(...)` calls so subscriptions clean up when the user navigates away. |
40-
| `src/lib/monaco-setup.ts` | Configures Monaco's TS worker: registers the bundled `@parity/truapi` types (`truapi-dts`), every rxjs `.d.ts`, and an ambient `declare const truapi: Client` so examples typecheck without manual imports. Defines the light/dark themes that match the design tokens. |
41-
| `src/lib/auto-test.ts` | Runs each method's example and reports pass / fail. `runDiagnosis` runs every method one at a time: automatic methods first, then the methods that prompt the user (signing, permission/resource requests, `navigate_to`) last so each interaction completes in isolation. `runSingleTest` replays one method (used by the Diagnosis row replay). |
39+
| `src/lib/example-runner.ts` | Transpiles each rustdoc `ts` example via sucrase, runs it inside an `AsyncFunction` with `truapi`, `console`, rxjs, and an ambient `assert` as bindings. Failure is explicit: an example fails iff it throws (via `assert(...)`, a timeout, or any uncaught error); `console.*` is pure output. A tracking Proxy auto-unsubscribes inner `.subscribe(...)` calls so subscriptions clean up when the run ends or the user navigates away. |
40+
| `src/lib/monaco-setup.ts` | Configures Monaco's TS worker: registers the bundled `@parity/truapi` types (`truapi-dts`), every rxjs `.d.ts`, and an ambient block (`declare const truapi: Client`, `assert`, `crypto`, `Uint8Array` hex helpers) so examples typecheck without manual imports. Defines the light/dark themes that match the design tokens. |
41+
| `src/lib/auto-test.ts` | Runs each method's example and reports pass / fail. A method passes when its example resolves within the timeout and fails when it throws (the thrown/`assert` message plus any logs become the failure output); unary and subscription examples are awaited identically. `runDiagnosis` runs every method one at a time: automatic methods first, then the methods that prompt the user (signing, permission/resource requests, `navigate_to`) last so each interaction completes in isolation. `runSingleTest` replays one method (used by the Diagnosis row replay). |
4242
| `src/lib/diagnosis-report.ts` | Renders the diagnosis results as a copy-pasteable GitHub-flavoured markdown table: a `## Truapi <Web\|Desktop\|Android\|iOS> Diagnosis` title (host mode via `detectHostMode` — a native host (Electron UA or `__HOST_WEBVIEW_MARK__`) is split by user-agent into Desktop / Android / iOS, a browser iframe ⇒ Web) and one method/status row per method. Deterministic for a given set of results (no timestamp). Consumed by the explorer's matrix aggregator. |
43-
| `src/lib/result-status.ts` | Shared `errorTextFrom` helper. An example's Err surfaces as a `console.error` log or a returned neverthrow Err, not a throw, so both `MethodView` and `auto-test.ts` use this to tell a failed call from a successful one. |
4443
| `src/lib/host-api-bridge.ts` | Just `stringify`, the JSON-with-bigint helper shared across components. |
4544
| `src/components/ExampleEditor.tsx` | Monaco editor wrapper. Auto-folds `// #region helpers` blocks on mount. |
46-
| `src/components/MethodView.tsx` | Per-method view: signature link to cargo doc, Example / Output tabs, status LED, Run / Stop buttons. |
45+
| `src/components/MethodView.tsx` | Per-method view: signature link to cargo doc, Example / Output tabs, status LED, Run / Stop buttons. Output is the example's `console.*` log; an explicit `assert`/error throw flips the LED to error and shows the thrown message. |
4746
| `src/components/DiagnosisView.tsx` | Diagnosis screen (own sidebar entry): purpose + login/phone instructions, a Run button, a live per-method log (queued → processing… → success/failed) with per-row expand + replay, and a Copy report button. |
4847
| `src/components/ServiceTable.tsx` / `CommandPalette.tsx` | Method browser and ⌘K search. The browser also hosts the Diagnosis entry. |
4948
| `src/app/page.tsx` | Root: connection status, selection state, deep-link sync via `pushState` + `popstate`. |
@@ -68,9 +67,12 @@ ServiceTable click / CommandPalette / ?service=…&method=… URL
6867
→ MethodView mounts ExampleEditor with method.exampleSource
6968
→ user clicks Run
7069
→ example-runner: sucrase transpiles TS → wraps in AsyncFunction
71-
→ executes with ambient `truapi` (a Proxy over the singleton client)
72-
→ unary: awaits the Promise, renders the Result via result.match
73-
→ subscription: returns a tracked Subscription; Stop calls unsubscribe
70+
→ executes with ambient `truapi` (a Proxy over the singleton client),
71+
`console`, rxjs, and `assert`
72+
→ awaits the returned Promise (subscriptions self-await their first
73+
event via `firstValueFrom`); console output streams into the panel
74+
→ resolves → success; throws (`assert`/error/timeout) → error
75+
→ Stop cancels the run and unsubscribes any tracked subscriptions
7476
```
7577

7678
A method without `exampleSource` shows a "Not supported" badge and disables the Run button. Adding support means writing a `ts` rustdoc block on the trait method.

playground/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ Methods reach the playground via codegen — there is no per-method wiring file
4545

4646
A method without a `ts` rustdoc block shows up with a "Not supported" badge — there is no example to run until you add one.
4747

48+
### Example conventions
49+
50+
An example **passes** when its promise resolves and **fails** when it throws. Use the ambient `assert(condition, ...message)` (no import) to fail explicitly — `assert(false, ...)` throws. `console.*` is pure output. For a `Result`, write `assert(r.isOk(), "<step> failed:", r)` (narrows `r` to `Ok`, includes the result in the failure message). Await subscriptions with `firstValueFrom(from(<observable>))`.
51+
4852
## Diagnosis
4953

5054
The Diagnosis view exercises every TrUAPI method against the connected host and emits a per-host pass/fail report you can copy out. Per-host reports feed the explorer's **Compatibility** page, which renders the host × method matrix; aggregation lives in the explorer (see [`explorer/README.md`](../explorer/README.md#host-compatibility-matrix)).

playground/src/components/MethodView.tsx

Lines changed: 36 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,8 @@
33
import { useState, useEffect, useRef, useCallback } from "react";
44
import { stringify } from "@/src/lib/host-api-bridge";
55
import { ExampleEditor } from "@/src/components/ExampleEditor";
6-
import {
7-
runExample,
8-
type LogEntry,
9-
type RunSubscription,
10-
} from "@/src/lib/example-runner";
6+
import { runExample, type LogEntry } from "@/src/lib/example-runner";
117
import { getClient } from "@/src/lib/transport";
12-
import { errorTextFrom } from "@/src/lib/result-status";
138
import { methodTestId, revealInRail, serviceTestId } from "@/src/lib/rail";
149
import { services } from "@/src/lib/services";
1510
import type { MethodInfo, ServiceInfo } from "@/src/lib/services";
@@ -70,8 +65,6 @@ export function MethodView({
7065
const [logs, setLogs] = useState<LogEntry[]>([]);
7166
const [running, setRunning] = useState(false);
7267
const [error, setError] = useState("");
73-
const [result, setResult] = useState("");
74-
const [activeSub, setActiveSub] = useState<RunSubscription | null>(null);
7568
const [tab, setTab] = useState<"example" | "output">("example");
7669
const callAbortRef = useRef<((reason: string) => void) | null>(null);
7770
const cancelRunRef = useRef<(() => void) | null>(null);
@@ -80,16 +73,7 @@ export function MethodView({
8073
setSource(methodInfo?.exampleSource ?? "");
8174
setLogs([]);
8275
setError("");
83-
setResult("");
8476
setRunning(false);
85-
setActiveSub((prev) => {
86-
try {
87-
prev?.unsubscribe();
88-
} catch {
89-
/* benign */
90-
}
91-
return null;
92-
});
9377
callAbortRef.current?.("method changed");
9478
callAbortRef.current = null;
9579
cancelRunRef.current?.();
@@ -98,17 +82,6 @@ export function MethodView({
9882
// eslint-disable-next-line react-hooks/exhaustive-deps
9983
}, [service, method]);
10084

101-
useEffect(
102-
() => () => {
103-
try {
104-
activeSub?.unsubscribe();
105-
} catch {
106-
/* benign */
107-
}
108-
},
109-
[activeSub],
110-
);
111-
11285
useEffect(
11386
() => () => {
11487
cancelRunRef.current?.();
@@ -137,61 +110,35 @@ export function MethodView({
137110

138111
const runnable = !!methodInfo?.exampleSource;
139112

113+
// Failure is explicit: the example resolves on success and throws (via
114+
// `assert(...)`, a timeout, or any uncaught error) on failure. `console.*`
115+
// output is captured into `logs` for display but never decides pass/fail.
140116
const handleRun = async () => {
141117
if (!runnable || !methodInfo) return;
142118
setRunning(true);
143119
setError("");
144-
setResult("");
145120
setLogs([]);
146121
setTab("output");
147-
// Examples self-handle their Result (`result.match(v => console.log(v),
148-
// e => console.error(e))`), so an Err surfaces as an error-level log rather
149-
// than a thrown exception. Accumulate logs locally — the `logs` React state
150-
// is stale inside this handler — so we can detect the error after the call.
151-
const callLogs: LogEntry[] = [];
152-
const collectLog = (entry: LogEntry) => {
153-
callLogs.push(entry);
154-
onLog(entry);
155-
};
156122
try {
157-
const client = getClient();
158-
const run = await runExample({
159-
source,
160-
kind: methodInfo.type,
161-
client,
162-
onLog: collectLog,
123+
const run = await runExample({ source, client: getClient(), onLog });
124+
cancelRunRef.current = run.cancel;
125+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
126+
const abortPromise = new Promise<never>((_, reject) => {
127+
callAbortRef.current = (reason: string) => reject(new Error(reason));
128+
timeoutHandle = setTimeout(
129+
() =>
130+
reject(new Error(`Call timed out after ${CALL_TIMEOUT_MS / 1000}s`)),
131+
CALL_TIMEOUT_MS,
132+
);
163133
});
164-
165-
if (run.kind === "unary") {
166-
cancelRunRef.current = run.cancel;
167-
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
168-
const abortPromise = new Promise<never>((_, reject) => {
169-
callAbortRef.current = (reason: string) => reject(new Error(reason));
170-
timeoutHandle = setTimeout(
171-
() =>
172-
reject(
173-
new Error(`Call timed out after ${CALL_TIMEOUT_MS / 1000}s`),
174-
),
175-
CALL_TIMEOUT_MS,
176-
);
177-
});
178-
try {
179-
const value = await Promise.race([run.promise, abortPromise]);
180-
const errText = errorTextFrom(value, callLogs);
181-
if (errText != null) {
182-
setError(errText);
183-
} else {
184-
const rendered = stringify(value);
185-
if (rendered !== undefined) setResult(rendered);
186-
}
187-
} finally {
188-
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
189-
callAbortRef.current = null;
190-
cancelRunRef.current = null;
191-
setRunning(false);
192-
}
193-
} else {
194-
setActiveSub(run.subscription);
134+
try {
135+
await Promise.race([run.promise, abortPromise]);
136+
} finally {
137+
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
138+
callAbortRef.current = null;
139+
run.cancel();
140+
cancelRunRef.current = null;
141+
setRunning(false);
195142
}
196143
} catch (err) {
197144
setError(formatError(err));
@@ -200,33 +147,18 @@ export function MethodView({
200147
};
201148

202149
const handleStop = () => {
203-
if (callAbortRef.current) {
204-
callAbortRef.current("Call aborted");
205-
return;
206-
}
207-
if (activeSub) {
208-
try {
209-
activeSub.unsubscribe();
210-
} catch {
211-
/* benign */
212-
}
213-
setActiveSub(null);
214-
setRunning(false);
215-
setLogs((prev) => [...prev, { level: "log", text: "--- stopped ---" }]);
216-
}
150+
callAbortRef.current?.("Call aborted");
217151
};
218152

219153
const kind = methodInfo?.type ?? "unary";
220154

221155
const status: Status = error
222156
? "error"
223-
: activeSub
224-
? "streaming"
225-
: running
226-
? "running"
227-
: result
228-
? "success"
229-
: "idle";
157+
: running
158+
? "running"
159+
: logs.length > 0
160+
? "success"
161+
: "idle";
230162

231163
return (
232164
<div>
@@ -344,28 +276,6 @@ export function MethodView({
344276
<button type="button" className="btn btn--primary" disabled>
345277
Not supported
346278
</button>
347-
) : kind === "subscription" ? (
348-
activeSub ? (
349-
<button
350-
type="button"
351-
className="btn btn--stop"
352-
data-testid="stop-button"
353-
onClick={handleStop}
354-
>
355-
<span className="btn__glyph"></span>
356-
Stop
357-
</button>
358-
) : (
359-
<button
360-
type="button"
361-
className="btn btn--primary"
362-
data-testid="subscribe-button"
363-
onClick={handleRun}
364-
>
365-
<span className="btn__glyph"></span>
366-
Run example
367-
</button>
368-
)
369279
) : running ? (
370280
<button
371281
type="button"
@@ -380,10 +290,14 @@ export function MethodView({
380290
<button
381291
type="button"
382292
className="btn btn--primary"
383-
data-testid="call-button"
293+
data-testid={
294+
kind === "subscription" ? "subscribe-button" : "call-button"
295+
}
384296
onClick={handleRun}
385297
>
386-
<span className="btn__glyph"></span>
298+
<span className="btn__glyph">
299+
{kind === "subscription" ? "●" : "→"}
300+
</span>
387301
Run example
388302
</button>
389303
)}
@@ -411,10 +325,6 @@ export function MethodView({
411325
</div>
412326
)}
413327
</div>
414-
) : result ? (
415-
<div className="console__body" data-testid="response-content">
416-
{result}
417-
</div>
418328
) : logs.length > 0 ? (
419329
<div className="console__body" data-testid="stream-log">
420330
{logs.map((entry, i) => (
@@ -436,9 +346,7 @@ export function MethodView({
436346
? "This method has no runnable example yet."
437347
: status === "running"
438348
? "Waiting for response…"
439-
: status === "streaming"
440-
? "Waiting for first event…"
441-
: "Run the example to see output here."}
349+
: "Run the example to see output here."}
442350
</div>
443351
)}
444352
</div>
@@ -448,12 +356,11 @@ export function MethodView({
448356
);
449357
}
450358

451-
type Status = "idle" | "running" | "streaming" | "success" | "error";
359+
type Status = "idle" | "running" | "success" | "error";
452360

453361
const LED_LABEL: Record<Status, string> = {
454362
idle: "Idle",
455363
running: "Running",
456-
streaming: "Streaming",
457364
success: "Success",
458365
error: "Error",
459366
};

0 commit comments

Comments
 (0)