Skip to content
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1e606b5
Enable plaintext JSON if requested
rbtying Sep 11, 2025
8258aa9
feat: Add support for environments without WebAssembly
rbtying Sep 12, 2025
2f430c8
refactor: Extract WASM RPC types into shared backend-types crate
rbtying Sep 12, 2025
0010d0c
refactor: Extract shared WASM RPC implementations into separate crate
rbtying Sep 12, 2025
23cbfed
test: Add tests for /api/rpc endpoint
rbtying Sep 12, 2025
1d273fe
feat: Add async provider with WASM/RPC fallback
rbtying Sep 12, 2025
76e7972
fix: Resolve compilation errors in async implementation
rbtying Sep 12, 2025
3d9985f
feat: Update BidArea and Cards components to use async WASM functions
rbtying Sep 12, 2025
6ddc2db
feat: Implement async getCardInfo with caching in Card component
rbtying Sep 12, 2025
c9701b7
fix: Improve Card component caching and reduce duplication
rbtying Sep 12, 2025
50d76a0
Convert frontend to async engine with RPC fallback for non-WASM envir…
rbtying Sep 12, 2025
ba205ae
Apply code formatting
rbtying Sep 12, 2025
69dd3cc
Fix ESLint config to align with Prettier formatting
rbtying Sep 12, 2025
bea5955
Fix TypeScript any types introduced during async conversion
rbtying Sep 12, 2025
2ccc236
Apply Prettier formatting
rbtying Sep 12, 2025
fa20496
Fix Rust clippy warnings
rbtying Sep 12, 2025
9fd3923
Fix Docker build by copying wasm-rpc-impl directory
rbtying Sep 12, 2025
9531d9f
Convert shengji-wasm to dynamic import
rbtying Sep 13, 2025
f35b70e
Merge master and resolve conflicts for dynamic WASM import
rbtying Sep 13, 2025
ae321fa
Add newline at end of file (formatting)
rbtying Sep 13, 2025
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
76 changes: 53 additions & 23 deletions frontend/src/WasmOrRpcProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from "react";
import * as Shengji from "../shengji-wasm/pkg/shengji-core.js";
import WasmContext from "./WasmContext";
import { isWasmAvailable } from "./detectWasm";
import {
Expand Down Expand Up @@ -35,6 +34,9 @@ interface IProps {
children: React.ReactNode;
}

// Type for the dynamically imported WASM module
type ShengjiModule = typeof import("../shengji-wasm/pkg/shengji-core.js");

// Define the RPC request types
type WasmRpcRequest =
| ({ type: "FindViablePlays" } & FindViablePlaysRequest)
Expand Down Expand Up @@ -75,60 +77,63 @@ async function callRpc<T>(request: WasmRpcRequest): Promise<T> {
}

// Create async versions of each function that can fallback to RPC
const createAsyncFunctions = (useWasm: boolean) => {
if (useWasm) {
// WASM is available, use synchronous WASM functions wrapped in promises
const createAsyncFunctions = (
useWasm: boolean,
wasmModule: ShengjiModule | null,
) => {
if (useWasm && wasmModule) {
// WASM is available and loaded, use synchronous WASM functions wrapped in promises
return {
findViablePlays: async (
trump: Trump,
tractorRequirements: TractorRequirements,
cards: string[],
): Promise<FoundViablePlay[]> => {
return Shengji.find_viable_plays({
return wasmModule.find_viable_plays({
trump,
cards,
tractor_requirements: tractorRequirements,
}).results;
},
findValidBids: async (req: FindValidBidsRequest): Promise<Bid[]> => {
return Shengji.find_valid_bids(req).results;
return wasmModule.find_valid_bids(req).results;
},
sortAndGroupCards: async (
req: SortAndGroupCardsRequest,
): Promise<SuitGroup[]> => {
return Shengji.sort_and_group_cards(req).results;
return wasmModule.sort_and_group_cards(req).results;
},
decomposeTrickFormat: async (
req: DecomposeTrickFormatRequest,
): Promise<DecomposedTrickFormat[]> => {
return Shengji.decompose_trick_format(req).results;
return wasmModule.decompose_trick_format(req).results;
},
canPlayCards: async (req: CanPlayCardsRequest): Promise<boolean> => {
return Shengji.can_play_cards(req).playable;
return wasmModule.can_play_cards(req).playable;
},
explainScoring: async (
req: ExplainScoringRequest,
): Promise<ExplainScoringResponse> => {
return Shengji.explain_scoring(req);
return wasmModule.explain_scoring(req);
},
nextThresholdReachable: async (
req: NextThresholdReachableRequest,
): Promise<boolean> => {
return Shengji.next_threshold_reachable(req);
return wasmModule.next_threshold_reachable(req);
},
computeScore: async (
req: ComputeScoreRequest,
): Promise<ComputeScoreResponse> => {
return Shengji.compute_score(req);
return wasmModule.compute_score(req);
},
computeDeckLen: async (decks: Deck[]): Promise<number> => {
return Shengji.compute_deck_len({ decks });
return wasmModule.compute_deck_len({ decks });
},
batchGetCardInfo: async (
req: BatchCardInfoRequest,
): Promise<BatchCardInfoResponse> => {
// WASM doesn't have batch API, so call individually
const results = req.requests.map((r) => Shengji.get_card_info(r));
const results = req.requests.map((r) => wasmModule.get_card_info(r));
return { results };
},
};
Expand Down Expand Up @@ -255,17 +260,41 @@ export const EngineContext = React.createContext<EngineContext | null>(null);

const WasmOrRpcProvider = (props: IProps): JSX.Element => {
const useWasm = isWasmAvailable();
const [wasmModule, setWasmModule] = React.useState<ShengjiModule | null>(
null,
);
const [isLoading, setIsLoading] = React.useState(useWasm);

// Load WASM module dynamically if available
React.useEffect(() => {
if (useWasm) {
import("../shengji-wasm/pkg/shengji-core.js")
.then((module) => {
setWasmModule(module);
// Set module on window for debugging
(window as Window & { shengji?: ShengjiModule }).shengji = module;
setIsLoading(false);
})
.catch((error) => {
console.error("Failed to load WASM module:", error);
setIsLoading(false);
});
} else {
setIsLoading(false);
}
}, [useWasm]);

const engineFuncs = React.useMemo(
() => createAsyncFunctions(useWasm),
[useWasm],
() => createAsyncFunctions(useWasm, wasmModule),
[useWasm, wasmModule],
);

// Only provide decodeWireFormat in the synchronous context
const syncContextValue = React.useMemo(
() => ({
decodeWireFormat: (req: Uint8Array) => {
if (useWasm) {
return JSON.parse(Shengji.zstd_decompress(req));
if (useWasm && wasmModule) {
return JSON.parse(wasmModule.zstd_decompress(req));
} else {
// When WASM is not available, messages should already be decompressed
// by the server, so we can just parse them directly
Expand All @@ -274,20 +303,21 @@ const WasmOrRpcProvider = (props: IProps): JSX.Element => {
}
},
}),
[useWasm],
[useWasm, wasmModule],
);

const engineContextValue: EngineContext = React.useMemo(
() => ({
...engineFuncs,
decodeWireFormat: syncContextValue.decodeWireFormat,
isUsingWasm: useWasm,
isUsingWasm: useWasm && wasmModule !== null,
}),
[engineFuncs, syncContextValue, useWasm],
[engineFuncs, syncContextValue, useWasm, wasmModule],
);

if (useWasm) {
(window as Window & { shengji?: typeof Shengji }).shengji = Shengji;
// Show loading indicator while WASM is being loaded
if (isLoading) {
return <div>Loading game engine...</div>;
}

return (
Expand Down
Loading