2022-06-02 21:09:27 +00:00
|
|
|
# LRPCResponseType represents the various types an LRPC
|
|
|
|
# response can have.
|
|
|
|
LRPCResponseType = {
|
|
|
|
Normal: 0,
|
|
|
|
Error: 1,
|
|
|
|
Channel: 2,
|
|
|
|
ChannelDone: 3,
|
|
|
|
}
|
|
|
|
|
|
|
|
# LRPCClient represents a client for the LRPC protocol
|
|
|
|
# using WebSockets and the JSON codec
|
|
|
|
class LRPCClient
|
|
|
|
def initialize(addr)
|
|
|
|
# Set self variables
|
|
|
|
@callMap = Map.new()
|
|
|
|
@enc = TextEncoder.new()
|
|
|
|
@dec = TextDecoder.new()
|
|
|
|
|
|
|
|
# Create connection to lrpc server
|
|
|
|
@conn = WebSocket.new(addr)
|
|
|
|
@conn.binaryType = "arraybuffer"
|
|
|
|
@conn.onmessage = proc do |msg|
|
|
|
|
# if msg.data is string
|
|
|
|
if msg.data.instance_of? String
|
|
|
|
# Set json to msg.data
|
|
|
|
json = msg.data
|
|
|
|
else
|
|
|
|
# Set json to decoded msg.data
|
|
|
|
json = @dec.decode(msg.data)
|
|
|
|
end
|
|
|
|
# Parse JSON string
|
|
|
|
val = JSON.parse(json)
|
|
|
|
# Get id from callMap
|
|
|
|
fns = @callMap.get(val.ID)
|
|
|
|
# If fns is undefined (key does not exist), and this is
|
|
|
|
# a normal response, return
|
|
|
|
return if !fns && val.Type == LRPCResponseType.Normal
|
|
|
|
|
|
|
|
case val.Type
|
|
|
|
when LRPCResponseType.Normal
|
|
|
|
# If fns is a channel, send the value. Otherwise,
|
|
|
|
# resolve the promise with the value.
|
|
|
|
if fns.isChannel
|
|
|
|
fns.send(val.Return)
|
|
|
|
else
|
|
|
|
fns.resolve(val.Return)
|
|
|
|
end
|
|
|
|
when LRPCResponseType.Channel
|
|
|
|
# Get channel ID from response
|
|
|
|
chID = val.Return
|
|
|
|
# Create new LRPCChannel
|
|
|
|
ch = LRPCChannel.new(self, chID)
|
|
|
|
# Set channel in map
|
|
|
|
@callMap.set(chID, ch)
|
|
|
|
# Resolve promise with channel
|
|
|
|
fns.resolve(ch)
|
|
|
|
when LRPCResponseType.ChannelDone
|
|
|
|
# Close and delete channel
|
|
|
|
fns.close()
|
|
|
|
@callMap.delete(val.ID)
|
|
|
|
when LRPCResponseType.Error
|
|
|
|
# Reject promise with error
|
|
|
|
fns.reject(val.Error)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Delete item from map unless it is a channel
|
|
|
|
@callMap.delete(val.ID) unless fns.isChannel
|
|
|
|
end
|
|
|
|
end
|
2022-06-03 01:54:00 +00:00
|
|
|
|
2022-06-02 21:09:27 +00:00
|
|
|
# call calls a method on the server with the given
|
|
|
|
# argument and returns a promise.
|
2022-06-03 01:54:00 +00:00
|
|
|
def callMethod(rcvr, method, arg)
|
2022-06-02 21:09:27 +00:00
|
|
|
return Promise.new do |resolve, reject|
|
|
|
|
# Get random UUID (this only works with TLS)
|
|
|
|
id = crypto.randomUUID()
|
|
|
|
# Add resolve/reject functions to callMap
|
|
|
|
@callMap.set(id, {
|
|
|
|
resolve: resolve,
|
|
|
|
reject: reject,
|
|
|
|
})
|
|
|
|
|
|
|
|
# Encode data as JSON
|
|
|
|
data = @enc.encode({
|
|
|
|
Receiver: rcvr,
|
|
|
|
Method: method,
|
|
|
|
Arg: arg,
|
|
|
|
ID: id,
|
|
|
|
}.to_json())
|
|
|
|
|
|
|
|
# Send data to lrpc server
|
|
|
|
@conn.send(data.buffer)
|
|
|
|
end
|
|
|
|
end
|
2022-06-04 00:59:58 +00:00
|
|
|
|
|
|
|
# getClient returns an object containing functions
|
|
|
|
# corresponding to registered functions on the given
|
|
|
|
# receiver. It uses the lrpc.Introspect() endpoint
|
|
|
|
# to achieve this.
|
|
|
|
def getObject(rcvr)
|
|
|
|
return Promise.new do |resolve|
|
|
|
|
# Introspect methods on given receiver
|
|
|
|
self.callMethod("lrpc", "Introspect", rcvr).then do |methodDesc|
|
|
|
|
# Create output object
|
|
|
|
out = {}
|
|
|
|
# For each method in description array
|
|
|
|
methodDesc.each do |method|
|
|
|
|
# Create and assign new function to call current method
|
|
|
|
out[method.Name] = proc { |arg| return self.callMethod(rcvr, method.Name, arg) }
|
|
|
|
end
|
|
|
|
# Resolve promise with output promise
|
|
|
|
resolve(out)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2022-06-02 21:09:27 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# LRPCChannel represents a channel used for lrpc.
|
|
|
|
class LRPCChannel
|
|
|
|
def initialize(client, id)
|
|
|
|
# Set self variables
|
|
|
|
@client = client
|
|
|
|
@id = id
|
2022-06-03 19:33:03 +00:00
|
|
|
@closed = false
|
2022-06-02 21:09:27 +00:00
|
|
|
# Set function variables to no-ops
|
|
|
|
@onMessage = proc {|fn|}
|
|
|
|
@onClose = proc {}
|
|
|
|
end
|
|
|
|
|
|
|
|
# isChannel is defined to allow identifying whether
|
|
|
|
# an object is a channel.
|
|
|
|
def isChannel() end
|
|
|
|
|
|
|
|
# send sends a value on the channel. This should not
|
|
|
|
# be called by the consumer of the channel.
|
|
|
|
def send(val)
|
2022-06-03 19:33:03 +00:00
|
|
|
return if @closed
|
2022-06-02 21:09:27 +00:00
|
|
|
fn = @onMessage
|
|
|
|
fn(val)
|
|
|
|
end
|
|
|
|
|
|
|
|
# done cancels the context corresponding to the channel
|
|
|
|
# on the server side and closes the channel.
|
|
|
|
def done()
|
2022-06-03 19:33:03 +00:00
|
|
|
return if @closed
|
2022-06-03 01:54:00 +00:00
|
|
|
@client.callMethod("lrpc", "ChannelDone", @id)
|
2022-06-02 21:09:27 +00:00
|
|
|
self.close()
|
|
|
|
@client._callMap.delete(@id)
|
|
|
|
end
|
|
|
|
|
|
|
|
# onMessage sets the callback to be called whenever a
|
|
|
|
# message is received. The function should have one parameter
|
|
|
|
# that will be set to the value received. Subsequent calls
|
|
|
|
# will overwrite the callback
|
|
|
|
def onMessage(fn)
|
|
|
|
@onMessage = fn
|
|
|
|
end
|
|
|
|
|
|
|
|
# onClose sets the callback to be called whenever the client
|
|
|
|
# is closed. The function should have no parameters.
|
|
|
|
# Subsequent calls will overwrite the callback
|
|
|
|
def onClose(fn)
|
|
|
|
@onClose = fn
|
|
|
|
end
|
|
|
|
|
|
|
|
# close closes the channel. This should not be called by the
|
|
|
|
# consumer of the channel. Use done() instead.
|
|
|
|
def close()
|
2022-06-03 21:36:34 +00:00
|
|
|
return if @closed
|
2022-06-02 21:09:27 +00:00
|
|
|
fn = @onClose
|
|
|
|
fn()
|
2022-06-03 19:33:03 +00:00
|
|
|
@closed = true
|
2022-06-02 21:09:27 +00:00
|
|
|
end
|
|
|
|
end
|