/// /** * TEST PATTERN: Verify "Never use await in handlers" claim * * CLAIM FROM DEBUGGING.md: * "Never use await in handlers (use fetchData instead)" * * This pattern tests whether await in handlers actually blocks the UI. * * MANUAL TEST INSTRUCTIONS: * * 1. Open this pattern in the runner * 2. Click "Test WITH await (Bad?)" button * - This triggers a handler with await and a 3-second delay * - While waiting, try clicking the counter increment button * - EXPECTED (if claim is TRUE): Counter button is unresponsive, UI frozen * - OBSERVED: [Record your findings here after testing] * * 3. Wait for the operation to complete * 4. Click "Test WITHOUT await (Good)" button * - This uses fetchData with the same 3-second delay * - While waiting, try clicking the counter increment button * - EXPECTED: Counter button works, UI responsive * - OBSERVED: [Record your findings here after testing] * * 5. Compare the two approaches * - Does await actually block the UI? * - Is fetchData actually non-blocking? * * WHAT TO LOOK FOR: * - Can you increment the counter while "Awaiting..." is displayed? * - Can you increment the counter while "Fetching..." is displayed? * - Does the UI feel frozen during the await operation? * - Are there visual differences in how the two approaches behave? * * CONCLUSION: * [Fill in after manual testing - does the claim hold up?] */ import { Cell, Default, derive, fetchData, handler, NAME, recipe, UI, } from "commontools"; // Handler WITH await (supposedly blocks UI) const testWithAwait = handler< unknown, { awaitStatus: Cell; awaitResult: Cell; awaitCount: Cell; } >(async (_args, state) => { // Increment counter to show how many times this was triggered state.awaitCount.set(state.awaitCount.get() + 1); state.awaitStatus.set("Awaiting..."); state.awaitResult.set(""); try { // THIS IS THE KEY TEST: Does await block the UI? await new Promise((resolve) => setTimeout(resolve, 3000)); state.awaitResult.set("Completed after 3000ms"); state.awaitStatus.set("Completed"); } catch (error) { state.awaitStatus.set("Error"); state.awaitResult.set(String(error)); } }); // Handler WITHOUT await (triggers reactive flow by changing state) // This demonstrates the correct pattern: handler is synchronous, // async work happens in the reactive layer via fetchData const testWithoutAwait = handler< unknown, { fetchTrigger: Cell; fetchCount: Cell; } >((_args, state) => { // Increment counter to show how many times this was triggered state.fetchCount.set(state.fetchCount.get() + 1); // Trigger by updating a cell - handler returns IMMEDIATELY // The actual async work happens in fetchData in the pattern body state.fetchTrigger.set(Date.now()); }); // Simple counter increment to test UI responsiveness const incrementCounter = handler }>( (_args, state) => { state.counter.set(state.counter.get() + 1); }, ); // Reset all state const resetAll = handler< unknown, { awaitStatus: Cell; awaitResult: Cell; awaitCount: Cell; fetchTrigger: Cell; fetchCount: Cell; counter: Cell; } >((_args, state) => { state.awaitStatus.set("Ready"); state.awaitResult.set(""); state.awaitCount.set(0); state.fetchTrigger.set(0); state.fetchCount.set(0); state.counter.set(0); }); interface PatternState { // State for await test awaitStatus: Default; awaitResult: Default; awaitCount: Default; // State for reactive/fetchData test fetchTrigger: Default; fetchCount: Default; // Interactive counter to test responsiveness counter: Default; } export default recipe("Await in Handlers Test", (state) => { // Build URL reactively from trigger - uses local /_health endpoint with delay const fetchUrl = derive( state.fetchTrigger, (trigger) => trigger ? `/_health?delay=3000&_=${trigger}` : "", ); // fetchData runs in the reactive layer - doesn't block the handler const fetchDataResult = fetchData>({ url: fetchUrl, mode: "json", }); const fetchStatus = derive( [fetchDataResult.pending, fetchDataResult.error, state.fetchTrigger], ([pending, error, trigger]) => { if (pending) return "Fetching..."; if (error) return "Error"; if (trigger) return "Completed"; return "Ready"; }, ); const fetchResultText = derive( [fetchDataResult.error, fetchDataResult.result, state.fetchCount], ([error, result, count]) => { if (error) return String(error); if (result) return `Fetched successfully (${count} triggers)`; return "(none)"; }, ); return { [NAME]: "Test: Await in Handlers", [UI]: (

Test: Does await Block UI in Handlers?

{/* All buttons together for quick clicking */}

Test Buttons (click test, then spam counter)

WITH await (3s) WITHOUT await (3s) Counter: {state.counter} Reset

Click a test button, then immediately spam the Counter button to test responsiveness

Test 1: WITH await (Supposedly Bad)

Status: {state.awaitStatus}
Result: {state.awaitResult || "(none)"}
Triggered: {state.awaitCount} times
Expected:{" "} Counter button unresponsive while "Awaiting..."

Test 2: WITHOUT await (Supposedly Good)

Status: {fetchStatus}
Result: {fetchResultText}
Triggered: {state.fetchCount} times
Expected:{" "} Counter button works while "Fetching..."

Expected Results (According to Claim)

  • WITH await:{" "} UI should freeze. Counter button unresponsive while "Awaiting..." is shown.
  • WITHOUT await (fetchData):{" "} UI should stay responsive. Counter button works while "Fetching..." is shown.
), }; });