///
/**
* 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.