1 // Package rpc is a client-only RPC package using websockets for javascript 2 package rpc // import "vimagination.zapto.org/gopherjs/rpc" 3 4 import ( 5 "encoding/json" 6 "errors" 7 8 "github.com/gopherjs/gopherjs/js" 9 "github.com/gopherjs/websocket/websocketjs" 10 ) 11 12 // Call represents the necessary data for an RPC call 13 type Call struct { 14 ServiceMethod string 15 Args, Reply interface{} 16 Error error 17 Done chan *Call 18 } 19 20 type request struct { 21 Method string `json:"method"` 22 ID uint `json:"id"` 23 Params [1]interface{} `json:"params"` 24 } 25 26 type response struct { 27 ID uint `json:"id"` 28 Result json.RawMessage `json:"result"` 29 Error string `json:"error"` 30 } 31 32 // Client is an RPC client 33 type Client struct { 34 ws *websocketjs.WebSocket 35 nextID uint 36 reqs map[uint]func(json.RawMessage, error) 37 } 38 39 // Dial connects a websocket to the given address and creates the client 40 func Dial(addr string) (*Client, error) { 41 w, err := websocketjs.New(addr) 42 if err != nil { 43 return nil, err 44 } 45 reqs := make(map[uint]func(json.RawMessage, error)) 46 ready := make(chan struct{}) 47 w.AddEventListener("open", false, func(*js.Object) { 48 close(ready) 49 }) 50 w.AddEventListener("message", false, func(e *js.Object) { 51 var r response 52 err := json.Unmarshal([]byte(e.Get("data").String()), &r) 53 f, ok := reqs[r.ID] 54 if ok { 55 if err == nil && len(r.Error) != 0 { 56 err = errors.New(r.Error) 57 } 58 delete(reqs, r.ID) 59 go f(r.Result, err) 60 } 61 }) 62 <-ready 63 return &Client{ 64 ws: w, 65 reqs: reqs, 66 }, nil 67 } 68 69 // Call make an RPC request to the given method name 70 func (c *Client) Call(method string, args interface{}, reply interface{}) error { 71 call := <-c.Go(method, args, reply, make(chan *Call, 1)).Done 72 return call.Error 73 } 74 75 // Close closes the websocket - it does not currently do anything special with 76 // outstanding requests 77 func (c *Client) Close() error { 78 return c.ws.Close() 79 } 80 81 // Go makes an RPC request in a goroutine, returning a Call for the user to 82 // be notified upon completion and to retrieve any errors returned. 83 // 84 // If a nil done chan is given, one will be created 85 func (c *Client) Go(method string, args interface{}, reply interface{}, done chan *Call) *Call { 86 call := &Call{ 87 ServiceMethod: method, 88 Args: args, 89 Reply: reply, 90 } 91 if done == nil { 92 call.Done = make(chan *Call, 1) 93 } else { 94 if cap(done) < 1 { 95 panic("invalid channel capacity") 96 } 97 call.Done = done 98 } 99 str, err := json.Marshal(request{ 100 Method: method, 101 ID: c.nextID, 102 Params: [1]interface{}{args}, 103 }) 104 if err == nil { 105 err = c.ws.Send(string(str)) 106 } 107 if err != nil { 108 call.Error = err 109 call.Done <- call 110 return call 111 } 112 c.reqs[c.nextID] = func(rm json.RawMessage, err error) { 113 if err != nil { 114 call.Error = err 115 } else if err = json.Unmarshal(rm, reply); err != nil { 116 call.Error = err 117 } 118 call.Done <- call 119 } 120 c.nextID++ 121 return call 122 }