|
| 1 | +//go:build js && wasm |
| 2 | + |
| 3 | +package main |
| 4 | + |
| 5 | +import ( |
| 6 | + "encoding/json" |
| 7 | + "errors" |
| 8 | + "io" |
| 9 | + "strings" |
| 10 | + "syscall/js" |
| 11 | + |
| 12 | + "gosuda.org/moreconsensus/visualizer/internal/sim" |
| 13 | +) |
| 14 | + |
| 15 | +type request struct { |
| 16 | + Op string `json:"op"` |
| 17 | + Scenario string `json:"scenario"` |
| 18 | + Size int `json:"size"` |
| 19 | + Index int `json:"index"` |
| 20 | + Action sim.Action `json:"action"` |
| 21 | +} |
| 22 | + |
| 23 | +type response struct { |
| 24 | + OK bool `json:"ok"` |
| 25 | + Scenarios []sim.ScenarioMeta `json:"scenarios,omitempty"` |
| 26 | + Trace *sim.ScenarioTrace `json:"trace,omitempty"` |
| 27 | + Frame *sim.Frame `json:"frame,omitempty"` |
| 28 | + CanBack *bool `json:"canBack,omitempty"` |
| 29 | + CanForward *bool `json:"canForward,omitempty"` |
| 30 | + Error *errorResponse `json:"error,omitempty"` |
| 31 | +} |
| 32 | + |
| 33 | +type errorResponse struct { |
| 34 | + Code string `json:"code"` |
| 35 | + Message string `json:"message"` |
| 36 | +} |
| 37 | + |
| 38 | +type bridge struct { |
| 39 | + lab *sim.Session |
| 40 | + labCursor int |
| 41 | + labLength int |
| 42 | +} |
| 43 | + |
| 44 | +var dispatchFunction js.Func |
| 45 | + |
| 46 | +func main() { |
| 47 | + state := &bridge{} |
| 48 | + dispatchFunction = js.FuncOf(state.dispatch) |
| 49 | + js.Global().Set("epaxosVizDispatch", dispatchFunction) |
| 50 | + document := js.Global().Get("document") |
| 51 | + event := js.Global().Get("CustomEvent").New("epaxos-viz-ready") |
| 52 | + document.Call("dispatchEvent", event) |
| 53 | + select {} |
| 54 | +} |
| 55 | + |
| 56 | +func (b *bridge) dispatch(_ js.Value, args []js.Value) (result any) { |
| 57 | + defer func() { |
| 58 | + if recovered := recover(); recovered != nil { |
| 59 | + result = marshalResponse(failure(sim.CodeInternal, "The EPaxos core rejected this request.")) |
| 60 | + } |
| 61 | + }() |
| 62 | + if len(args) != 1 || args[0].Type() != js.TypeString { |
| 63 | + return marshalResponse(failure(sim.CodeInvalidRequest, "Pass one JSON request string.")) |
| 64 | + } |
| 65 | + decoded, err := decodeRequest(args[0].String()) |
| 66 | + if err != nil { |
| 67 | + return marshalResponse(failure(sim.CodeInvalidRequest, "The request is not valid JSON.")) |
| 68 | + } |
| 69 | + return marshalResponse(b.handle(decoded)) |
| 70 | +} |
| 71 | + |
| 72 | +func decodeRequest(raw string) (request, error) { |
| 73 | + decoder := json.NewDecoder(strings.NewReader(raw)) |
| 74 | + decoder.DisallowUnknownFields() |
| 75 | + var decoded request |
| 76 | + if err := decoder.Decode(&decoded); err != nil { |
| 77 | + return request{}, err |
| 78 | + } |
| 79 | + if err := decoder.Decode(new(any)); !errors.Is(err, io.EOF) { |
| 80 | + return request{}, errors.New("request has trailing JSON") |
| 81 | + } |
| 82 | + return decoded, nil |
| 83 | +} |
| 84 | + |
| 85 | +func (b *bridge) handle(req request) response { |
| 86 | + switch req.Op { |
| 87 | + case "catalog": |
| 88 | + if req.Scenario != "" || req.Size != 0 || req.Index != 0 || req.Action != (sim.Action{}) { |
| 89 | + return failure(sim.CodeInvalidRequest, "Catalog does not accept additional fields.") |
| 90 | + } |
| 91 | + return response{OK: true, Scenarios: sim.Catalog()} |
| 92 | + case "scenario": |
| 93 | + if req.Scenario == "" || req.Size != 0 || req.Index != 0 || req.Action != (sim.Action{}) { |
| 94 | + return failure(sim.CodeInvalidRequest, "Choose one guided scenario.") |
| 95 | + } |
| 96 | + trace, err := sim.BuildScenario(req.Scenario) |
| 97 | + if err != nil { |
| 98 | + return fromError(err) |
| 99 | + } |
| 100 | + return response{OK: true, Trace: &trace} |
| 101 | + case "lab.reset": |
| 102 | + if req.Scenario != "" || req.Index != 0 || req.Action != (sim.Action{}) || (req.Size != 3 && req.Size != 5) { |
| 103 | + return failure(sim.CodeInvalidRequest, "Lab reset accepts only a cluster size.") |
| 104 | + } |
| 105 | + session, err := sim.NewSession(req.Size) |
| 106 | + if err != nil { |
| 107 | + return fromError(err) |
| 108 | + } |
| 109 | + frame, err := session.Seek(0) |
| 110 | + if err != nil { |
| 111 | + return fromError(err) |
| 112 | + } |
| 113 | + b.lab = session |
| 114 | + b.labCursor = 0 |
| 115 | + b.labLength = 0 |
| 116 | + return frameResponse(frame, false, false) |
| 117 | + case "lab.action": |
| 118 | + if req.Scenario != "" || req.Size != 0 || req.Index != 0 || b.lab == nil { |
| 119 | + return failure(sim.CodeInvalidRequest, "Reset the lab before dispatching an action.") |
| 120 | + } |
| 121 | + frame, err := b.lab.Dispatch(req.Action) |
| 122 | + if err != nil { |
| 123 | + return fromError(err) |
| 124 | + } |
| 125 | + b.labCursor = frame.Index |
| 126 | + b.labLength = frame.Index |
| 127 | + return frameResponse(frame, b.labCursor > 0, false) |
| 128 | + case "lab.seek": |
| 129 | + if req.Scenario != "" || req.Size != 0 || req.Action != (sim.Action{}) || b.lab == nil { |
| 130 | + return failure(sim.CodeInvalidRequest, "Reset the lab before seeking its history.") |
| 131 | + } |
| 132 | + frame, err := b.lab.Seek(req.Index) |
| 133 | + if err != nil { |
| 134 | + return fromError(err) |
| 135 | + } |
| 136 | + b.labCursor = frame.Index |
| 137 | + return frameResponse(frame, b.labCursor > 0, b.labCursor < b.labLength) |
| 138 | + default: |
| 139 | + return failure(sim.CodeInvalidRequest, "That operation is not supported.") |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +func frameResponse(frame sim.Frame, canBack, canForward bool) response { |
| 144 | + return response{OK: true, Frame: &frame, CanBack: &canBack, CanForward: &canForward} |
| 145 | +} |
| 146 | + |
| 147 | +func fromError(err error) response { |
| 148 | + code := sim.CodeInternal |
| 149 | + var coded interface{ Code() string } |
| 150 | + if errors.As(err, &coded) { |
| 151 | + code = coded.Code() |
| 152 | + } |
| 153 | + return failure(code, err.Error()) |
| 154 | +} |
| 155 | + |
| 156 | +func failure(code, message string) response { |
| 157 | + return response{OK: false, Error: &errorResponse{Code: code, Message: message}} |
| 158 | +} |
| 159 | + |
| 160 | +func marshalResponse(value response) string { |
| 161 | + encoded, err := json.Marshal(value) |
| 162 | + if err != nil { |
| 163 | + return `{"ok":false,"error":{"code":"internal","message":"The EPaxos response could not be encoded."}}` |
| 164 | + } |
| 165 | + return string(encoded) |
| 166 | +} |
0 commit comments