|
@ChromeRPC = {} |
|
|
|
class @ChromeRPC |
|
|
|
# Establish communication channel with server. |
|
constructor: (@options = {}) -> |
|
|
|
#@Message: |
|
# name: 'S.Data.RPC.Message' |
|
# properties: |
|
# method: {required: true} |
|
# params: {required: true} |
|
# id: {required: false} |
|
|
|
@options.logger or= console |
|
|
|
@logger = @options.logger |
|
|
|
# Listeners for incoming RPC requests. |
|
@listeners = {} |
|
|
|
# Dictionary of outgoing requests waiting for a response. |
|
@outgoing = {} |
|
|
|
# Dictionary of incoming requests to respond to. |
|
@incoming = {} |
|
|
|
start: (@socket) => |
|
|
|
# Validate socket conforms to API. |
|
unless @socket.on? and @socket.emit? and @socket.removeAllListeners? |
|
throw new Error "Socket does not conform to require API" |
|
|
|
@lastId = 0 |
|
@outgoing = {} |
|
@incoming = {} |
|
@socket.on 'rpc', @handle |
|
|
|
stop: => |
|
for key, callbacks of @outgoing |
|
callbacks.error?("socket disconnected") |
|
if @socket |
|
@socket.removeAllListeners() |
|
delete @socket |
|
|
|
# Add a listener for an rpc request |
|
# TODO: Only one listener for each request |
|
on: (method, cb) => |
|
#validateArgs 2, ['string', 'function'] |
|
|
|
if @listeners[method]? |
|
throw new Error "There is already a listener for '#{method}'. " + |
|
"Only one listener permitted for each request." |
|
@listeners[method] = cb |
|
|
|
### |
|
Handle incoming message from server |
|
@param {S.Data.RPC.Message} message - JSON-RPC message |
|
### |
|
handle: (message) => |
|
#validateArgs 1, [S.Data.RPC.Message] |
|
|
|
# Check if we have a request rather than a response |
|
if message.method? |
|
if message.id? |
|
@logger.debug "Received request:", message.method, message.id |
|
else |
|
@logger.debug "Received notification:", message.method |
|
fn = @listeners[message.method] |
|
# Do we have a handler for this request |
|
if not fn? |
|
@logger.error "No listener found for #{message.method}" |
|
fn = (params, callback) -> callback("no listener found") |
|
|
|
# Check if we need to reply with a response |
|
callback = null |
|
if message.id? |
|
callback = (err, res) => |
|
response = |
|
result: res |
|
error: err |
|
id: message.id |
|
@socket.emit 'rpc', response |
|
fn message.params, callback |
|
return |
|
|
|
# We must have a response |
|
else |
|
|
|
# TODO: Report method name of response. Would make it way |
|
# easier for debugging. |
|
@logger.debug "Received response:", message.id, message |
|
# Get callbacks for given request id |
|
callbacks = @outgoing[message.id] |
|
delete @outgoing[message.id] |
|
|
|
# No callbacks found - carry on without error |
|
return if not callbacks? |
|
|
|
# If we've encountered an error in the response, trigger the error callback if it exists |
|
if message.error |
|
@logger.error "ERROR:", message.error |
|
callbacks.error?(message.error) |
|
return |
|
|
|
# Otherwise, successful request, run the success request if it exists |
|
callbacks.success?(null, message.result) |
|
|
|
# Send a message to the server |
|
# method - |
|
# params - |
|
# cb(err, resp) - [optional] If omitted, a notification is sent which |
|
# does not expect a response. |
|
call: (method, params, cb) => |
|
#validateArgs 1, 2, ['string', 'object'] |
|
|
|
if typeof params is 'function' |
|
throw new Error 'Message must not be a function' |
|
|
|
if @options.namespace? |
|
methodName = @options.namespace + '.' + method |
|
else |
|
methodName = method |
|
|
|
message = |
|
method: methodName |
|
|
|
if cb? then message.id = ++@lastId |
|
|
|
message.params ?= params |
|
|
|
# Send message to server |
|
if @socket? |
|
@logger.debug "Sending:", message |
|
@socket.emit 'rpc', message, (err, result) => |
|
if err? then @logger.error "Failed to send Socket.IO message: #{err}" |
|
# Store rpc callback with message id |
|
if message.id? |
|
id = message.id |
|
@outgoing[id] = |
|
success: cb |
|
error: cb |
|
else |
|
@logger.error "No connection" |
|
cb("no connection", null) |
|
|
|
setLogger: (@logger) => |
|
|
|
class ExampleSocket |
|
on: (msg, requestHandler) -> |
|
emit: (msg, obj, responseHandler) -> |
|
removeAllListeners: -> |
|
|
|
class @ChromeRPC.WindowSocket |
|
|
|
constructor: (@window, @receiver) -> |
|
|
|
on: (method, handler) => |
|
@window.onmessage = (event) => |
|
handler event.data |
|
|
|
# NOTE: ackCallback is only used to check successful send for supported |
|
# protocols like Socket.io. |
|
emit: (msg, message, ackCallback) => |
|
# NOTE: Message must be cloneable or you will get a `DataCloneError`. |
|
@receiver.postMessage message, '*' |
|
|
|
removeAllListeners: => |
|
# TODO |
|
|
|
@ChromeRPC.PackagedAppRPC = (receiver) => |
|
rpc = new @ChromeRPC |
|
rpc.start new ChromeRPC.WindowSocket window, receiver |
|
return rpc |
That's HAWT