diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abef2cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/client/web/lrpc.js +/s +/c \ No newline at end of file diff --git a/client/web/Gemfile b/client/web/Gemfile new file mode 100644 index 0000000..389519a --- /dev/null +++ b/client/web/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'ruby2js' \ No newline at end of file diff --git a/client/web/Gemfile.lock b/client/web/Gemfile.lock new file mode 100644 index 0000000..4157b57 --- /dev/null +++ b/client/web/Gemfile.lock @@ -0,0 +1,19 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + parser (3.1.2.0) + ast (~> 2.4.1) + regexp_parser (2.1.1) + ruby2js (5.0.1) + parser + regexp_parser (~> 2.1.1) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + ruby2js + +BUNDLED WITH + 2.3.15 diff --git a/client/web/Makefile b/client/web/Makefile new file mode 100644 index 0000000..0d35b39 --- /dev/null +++ b/client/web/Makefile @@ -0,0 +1,3 @@ +lrpc.js: convert.rb lrpc.rb + bundle install + ruby convert.rb > lrpc.js \ No newline at end of file diff --git a/client/web/convert.rb b/client/web/convert.rb new file mode 100755 index 0000000..3052479 --- /dev/null +++ b/client/web/convert.rb @@ -0,0 +1,6 @@ +#!/usr/bin/ruby + +require 'ruby2js' +require 'ruby2js/filter/functions' + +puts Ruby2JS.convert(File.read('lrpc.rb'), eslevel: 2016) \ No newline at end of file diff --git a/client/web/lrpc.rb b/client/web/lrpc.rb new file mode 100644 index 0000000..6d4ff40 --- /dev/null +++ b/client/web/lrpc.rb @@ -0,0 +1,150 @@ +# 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 + + # call calls a method on the server with the given + # argument and returns a promise. + def call(rcvr, method, arg) + 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 +end + +# LRPCChannel represents a channel used for lrpc. +class LRPCChannel + def initialize(client, id) + # Set self variables + @client = client + @id = id + @arr = [] + # 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) + @arr.push(val) + fn = @onMessage + fn(val) + end + + # done cancels the context corresponding to the channel + # on the server side and closes the channel. + def done() + @client.call("lrpc", "ChannelDone", @id) + 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() + fn = @onClose + fn() + end +end \ No newline at end of file