Skip to content

Commit 007e6d2

Browse files
rbtyingclaude
andauthored
Fix no-wasm support (lots of bugs, sad) (#484)
* Add no-WASM mode with automatic cache prefilling - Add ?no-wasm=true URL parameter to force RPC mode for debugging - Fix RPC serialization issues with HashMap<PlayerID, _> by adding custom Deserialize - Implement automatic cache prefilling when cards encounter uncached trumps - Prevent O(n) duplicate requests with promise-based tracking mechanism - Ensure cards wait for active prefills instead of making individual requests 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Optimize Cards component to avoid redundant sortAndGroupCards calls - Use stable string key for cards in hand to prevent unnecessary re-renders - Only re-run sorting when actual card content changes, not just object references - This reduces repeated sortAndGroupCards calls during gameplay 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Fix memoization in Cards component - Create stable key from actual hand data (card->count mapping) instead of derived array - Prevents unnecessary re-renders when hands object reference changes but content is same - Properly memoizes the key generation to avoid recreating it every render 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Improve RPC error handling to diagnose NextThresholdReachable issue - Better error messages when JSON parsing fails - Log the actual response text for debugging - This will help identify if server is returning non-JSON error messages 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Fix NextThresholdReachable serialization issue - Wrap boolean response in a struct to work with serde tag="type" - Update both RPC and WASM versions to return { reachable: bool } - Fix frontend to handle the new response structure - Add better error logging to diagnose JSON parsing issues The issue was that serde cannot serialize a primitive boolean as a tagged enum variant. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Fix WASM/RPC optimizations and reduce console logging - Add ?no-wasm=true URL parameter support for debugging RPC mode - Fix duplicate batchGetCardInfo requests by tracking active prefills - Optimize React dependency tracking to prevent unnecessary re-renders - Fix NextThresholdReachable RPC serialization error - Ensure WASM and RPC versions maintain consistent API - Remove excessive console.log statements (keeping no-WASM mode log) - Update TypeScript types for new response structures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * delete unnecessary files * format --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 852ef1e commit 007e6d2

File tree

14 files changed

+493
-179
lines changed

14 files changed

+493
-179
lines changed

backend/backend-types/src/wasm_rpc.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ pub enum WasmRpcRequest {
193193
BatchGetCardInfo(BatchCardInfoRequest),
194194
}
195195

196+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
197+
pub struct NextThresholdReachableResponse {
198+
pub reachable: bool,
199+
}
200+
196201
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
197202
#[serde(tag = "type")]
198203
pub enum WasmRpcResponse {
@@ -201,7 +206,7 @@ pub enum WasmRpcResponse {
201206
CanPlayCards(CanPlayCardsResponse),
202207
FindValidBids(FindValidBidsResult),
203208
SortAndGroupCards(SortAndGroupCardsResponse),
204-
NextThresholdReachable(bool),
209+
NextThresholdReachable(NextThresholdReachableResponse),
205210
ExplainScoring(ExplainScoringResponse),
206211
ComputeScore(ComputeScoreResponse),
207212
ComputeDeckLen(ComputeDeckLenResponse),

backend/src/wasm_rpc_handler.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use axum::{http::StatusCode, response::IntoResponse, Json};
2-
use shengji_types::wasm_rpc::{BatchCardInfoResponse, WasmRpcRequest, WasmRpcResponse};
2+
use shengji_types::wasm_rpc::{
3+
BatchCardInfoResponse, NextThresholdReachableResponse, WasmRpcRequest, WasmRpcResponse,
4+
};
35

46
pub async fn handle_wasm_rpc(Json(request): Json<WasmRpcRequest>) -> impl IntoResponse {
57
match process_request(request) {
@@ -29,7 +31,9 @@ fn process_request(request: WasmRpcRequest) -> Result<WasmRpcResponse, String> {
2931
wasm_rpc_impl::sort_and_group_cards(req),
3032
)),
3133
WasmRpcRequest::NextThresholdReachable(req) => Ok(WasmRpcResponse::NextThresholdReachable(
32-
wasm_rpc_impl::next_threshold_reachable(req)?,
34+
NextThresholdReachableResponse {
35+
reachable: wasm_rpc_impl::next_threshold_reachable(req)?,
36+
},
3337
)),
3438
WasmRpcRequest::ExplainScoring(req) => Ok(WasmRpcResponse::ExplainScoring(
3539
wasm_rpc_impl::explain_scoring(req)?,

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"prettier": {},
3535
"scripts": {
3636
"build": "rimraf dist/ && webpack",
37+
"dev": "rimraf dist/ && webpack --mode=development",
3738
"watch": "rimraf dist/ && webpack --watch --mode=development",
3839
"types": "cargo run --bin shengji-json-schema --quiet src/gen-types.schema.json && npx json2ts src/gen-types.schema.json src/gen-types.d.ts && prettier src --write",
3940
"prettier": "prettier src",

frontend/shengji-wasm/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use ruzstd::streaming_decoder::StreamingDecoder;
88
use shengji_types::wasm_rpc::{
99
CanPlayCardsRequest, CardInfoRequest, ComputeDeckLenRequest, ComputeScoreRequest,
1010
DecomposeTrickFormatRequest, ExplainScoringRequest, FindValidBidsRequest,
11-
FindViablePlaysRequest, NextThresholdReachableRequest, SortAndGroupCardsRequest,
11+
FindViablePlaysRequest, NextThresholdReachableRequest, NextThresholdReachableResponse,
12+
SortAndGroupCardsRequest,
1213
};
1314
use shengji_types::ZSTD_ZSTD_DICT;
1415
use wasm_bindgen::prelude::*;
@@ -65,9 +66,15 @@ pub fn sort_and_group_cards(req: JsValue) -> Result<JsValue, JsValue> {
6566
}
6667

6768
#[wasm_bindgen]
68-
pub fn next_threshold_reachable(req: JsValue) -> Result<bool, JsValue> {
69+
pub fn next_threshold_reachable(req: JsValue) -> Result<JsValue, JsValue> {
6970
let request: NextThresholdReachableRequest = req.into_serde().map_err(|e| e.to_string())?;
70-
wasm_rpc_impl::next_threshold_reachable(request).map_err(|e| JsValue::from_str(&e))
71+
let reachable =
72+
wasm_rpc_impl::next_threshold_reachable(request).map_err(|e| JsValue::from_str(&e))?;
73+
// Return the same structure as the RPC version: { reachable: bool }
74+
Ok(
75+
JsValue::from_serde(&NextThresholdReachableResponse { reachable })
76+
.map_err(|e| e.to_string())?,
77+
)
7178
}
7279

7380
#[wasm_bindgen]

frontend/src/Card.tsx

Lines changed: 153 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { SettingsContext } from "./AppStateProvider";
88
import { ISuitOverrides } from "./state/Settings";
99
import { Trump, CardInfo } from "./gen-types";
1010
import { useEngine } from "./useEngine";
11-
import { cardInfoCache, getTrumpKey } from "./util/cachePrefill";
11+
import {
12+
cardInfoCache,
13+
getTrumpKey,
14+
prefillCardInfoCache,
15+
getPrefillPromise,
16+
} from "./util/cachePrefill";
1217

1318
import type { JSX } from "react";
1419

@@ -38,34 +43,91 @@ const Card = (props: IProps): JSX.Element => {
3843

3944
React.useEffect(() => {
4045
// Only load card info if the card is in the lookup
41-
if (props.card in cardLookup) {
42-
// Check cache first
43-
if (cacheKey in cardInfoCache) {
44-
setCardInfo(cardInfoCache[cacheKey]);
45-
setIsLoading(false);
46-
return;
47-
}
46+
if (!(props.card in cardLookup)) {
47+
return;
48+
}
49+
50+
// Check cache first
51+
if (cacheKey in cardInfoCache) {
52+
setCardInfo(cardInfoCache[cacheKey]);
53+
setIsLoading(false);
54+
return;
55+
}
56+
57+
setIsLoading(true);
4858

49-
setIsLoading(true);
50-
engine
51-
.batchGetCardInfo({
52-
requests: [
53-
{
54-
card: props.card,
55-
trump: props.trump,
56-
},
57-
],
59+
// Check if a prefill is already in progress for this trump
60+
const existingPrefillPromise = getPrefillPromise(props.trump);
61+
if (existingPrefillPromise) {
62+
// Wait for existing prefill
63+
existingPrefillPromise
64+
.then(() => {
65+
// Check if our card is now cached
66+
if (cacheKey in cardInfoCache) {
67+
setCardInfo(cardInfoCache[cacheKey]);
68+
setIsLoading(false);
69+
} else {
70+
// If still not cached after prefill, something went wrong
71+
console.error(
72+
`Card ${props.card} not in cache after prefill completed`,
73+
);
74+
const staticInfo = cardLookup[props.card];
75+
setCardInfo({
76+
suit: null,
77+
effective_suit: "Unknown" as any,
78+
value: staticInfo.value || props.card,
79+
display_value: staticInfo.display_value || props.card,
80+
typ: staticInfo.typ || props.card,
81+
number: staticInfo.number || null,
82+
points: staticInfo.points || 0,
83+
});
84+
setIsLoading(false);
85+
}
5886
})
59-
.then((response) => {
60-
const info = response.results[0];
61-
// Cache the result with the trump-specific key
62-
cardInfoCache[cacheKey] = info;
63-
setCardInfo(info);
87+
.catch((error) => {
88+
console.error("Failed to wait for prefill:", error);
6489
setIsLoading(false);
90+
});
91+
return;
92+
}
93+
94+
// Check if we should trigger a full prefill for this trump
95+
const trumpKey = getTrumpKey(props.trump);
96+
97+
// Count how many cards are cached for this trump
98+
const cachedCount = Object.keys(cardInfoCache).filter((key) =>
99+
key.endsWith(`_${trumpKey}`),
100+
).length;
101+
102+
// If we have very few cached cards for this trump, prefill everything
103+
if (cachedCount < 5) {
104+
// Trigger full prefill for uncached trump
105+
106+
// Start the prefill and wait for it
107+
prefillCardInfoCache(engine, props.trump)
108+
.then(() => {
109+
// Check if our card is now cached
110+
if (cacheKey in cardInfoCache) {
111+
setCardInfo(cardInfoCache[cacheKey]);
112+
setIsLoading(false);
113+
} else {
114+
// Fallback if card still not in cache
115+
const staticInfo = cardLookup[props.card];
116+
setCardInfo({
117+
suit: null,
118+
effective_suit: "Unknown" as any,
119+
value: staticInfo.value || props.card,
120+
display_value: staticInfo.display_value || props.card,
121+
typ: staticInfo.typ || props.card,
122+
number: staticInfo.number || null,
123+
points: staticInfo.points || 0,
124+
});
125+
setIsLoading(false);
126+
}
65127
})
66128
.catch((error) => {
67-
console.error("Error getting card info:", error);
68-
// Fallback to basic info from static lookup
129+
console.error("Failed to prefill cache:", error);
130+
// Fallback on error
69131
const staticInfo = cardLookup[props.card];
70132
setCardInfo({
71133
suit: null,
@@ -78,7 +140,74 @@ const Card = (props: IProps): JSX.Element => {
78140
});
79141
setIsLoading(false);
80142
});
143+
return;
81144
}
145+
146+
// Only make individual request if no prefill is needed
147+
engine
148+
.batchGetCardInfo({
149+
requests: [
150+
{
151+
card: props.card,
152+
trump: props.trump,
153+
},
154+
],
155+
})
156+
.then((response) => {
157+
if (!response || !response.results || response.results.length === 0) {
158+
console.error("Invalid response from batchGetCardInfo:", response);
159+
// Fallback to basic info from static lookup
160+
const staticInfo = cardLookup[props.card];
161+
setCardInfo({
162+
suit: null,
163+
effective_suit: "Unknown" as any,
164+
value: staticInfo.value || props.card,
165+
display_value: staticInfo.display_value || props.card,
166+
typ: staticInfo.typ || props.card,
167+
number: staticInfo.number || null,
168+
points: staticInfo.points || 0,
169+
});
170+
setIsLoading(false);
171+
return;
172+
}
173+
const info = response.results[0];
174+
if (!info) {
175+
console.error("Card info is undefined in response:", response);
176+
// Fallback to basic info from static lookup
177+
const staticInfo = cardLookup[props.card];
178+
setCardInfo({
179+
suit: null,
180+
effective_suit: "Unknown" as any,
181+
value: staticInfo.value || props.card,
182+
display_value: staticInfo.display_value || props.card,
183+
typ: staticInfo.typ || props.card,
184+
number: staticInfo.number || null,
185+
points: staticInfo.points || 0,
186+
});
187+
setIsLoading(false);
188+
return;
189+
}
190+
// Cache the result with the trump-specific key
191+
cardInfoCache[cacheKey] = info;
192+
setCardInfo(info);
193+
setIsLoading(false);
194+
})
195+
.catch((error) => {
196+
console.error("Error getting card info:", error);
197+
console.error("Error stack:", error.stack);
198+
// Fallback to basic info from static lookup
199+
const staticInfo = cardLookup[props.card];
200+
setCardInfo({
201+
suit: null,
202+
effective_suit: "Unknown" as any,
203+
value: staticInfo.value || props.card,
204+
display_value: staticInfo.display_value || props.card,
205+
typ: staticInfo.typ || props.card,
206+
number: staticInfo.number || null,
207+
points: staticInfo.points || 0,
208+
});
209+
setIsLoading(false);
210+
});
82211
}, [cacheKey, props.card, props.trump, engine]);
83212

84213
if (!(props.card in cardLookup)) {

frontend/src/Cards.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ const Cards = (props: IProps): JSX.Element => {
6464
? cardsInHand
6565
: ArrayUtils.minus(cardsInHand, selectedCards);
6666

67+
// Create stable string representation of the player's hand for dependency checking
68+
// This prevents re-running the effect when hands.hands object reference changes
69+
// but the actual cards remain the same
70+
const handKey = React.useMemo(() => {
71+
if (!(props.playerId in hands.hands)) {
72+
return "";
73+
}
74+
// Create a stable key from the hand object (card -> count mapping)
75+
return Object.entries(hands.hands[props.playerId])
76+
.sort(([a], [b]) => a.localeCompare(b))
77+
.map(([card, count]) => `${card}:${count}`)
78+
.join(",");
79+
}, [hands.hands, props.playerId]);
80+
6781
// Load sorted cards when they change
6882
React.useEffect(() => {
6983
setIsLoading(true);
@@ -142,7 +156,7 @@ const Cards = (props: IProps): JSX.Element => {
142156
props.selectedCards,
143157
props.trump,
144158
props.playerId,
145-
hands.hands,
159+
handKey, // Use the stable key instead of hands.hands
146160
separateCardsBySuit,
147161
reverseCardOrder,
148162
engine,

frontend/src/Exchange.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,30 @@ import Players from "./Players";
1212
import LabeledPlay from "./LabeledPlay";
1313
import { ExchangePhase, Friend } from "./gen-types";
1414
import Cards from "./Cards";
15+
import { prefillCardInfoCache } from "./util/cachePrefill";
16+
import { useEngine } from "./useEngine";
1517

1618
import type { JSX } from "react";
1719

1820
interface IExchangeProps {
1921
state: ExchangePhase;
2022
name: string;
2123
}
24+
25+
// Wrapper component to handle cache prefilling with hooks
26+
function ExchangeWrapper(props: IExchangeProps) {
27+
const engine = useEngine();
28+
29+
React.useEffect(() => {
30+
if (props.state.trump && engine) {
31+
// Prefill cache for trump in Exchange component
32+
prefillCardInfoCache(engine, props.state.trump);
33+
}
34+
}, [props.state.trump, engine]);
35+
36+
return <Exchange {...props} />;
37+
}
38+
2239
interface IExchangeState {
2340
friends: Friend[];
2441
}
@@ -331,4 +348,4 @@ class Exchange extends React.Component<IExchangeProps, IExchangeState> {
331348
}
332349
}
333350

334-
export default Exchange;
351+
export default ExchangeWrapper;

frontend/src/Play.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const Play = (props: IProps): JSX.Element => {
120120

121121
// Only prefill if trump has changed
122122
if (trumpKey !== lastPrefillTrump) {
123-
console.log("Trump changed, prefilling caches...");
123+
// Trump changed, prefill caches
124124
setLastPrefillTrump(trumpKey);
125125

126126
// Prefill card info cache for all cards with the new trump

0 commit comments

Comments
 (0)