Skip to content

Instantly share code, notes, and snippets.

@vjpr
Last active June 27, 2022 10:01
Show Gist options
  • Save vjpr/8681912 to your computer and use it in GitHub Desktop.
Save vjpr/8681912 to your computer and use it in GitHub Desktop.
RPC for Chrome Packaged App to allow communication between sandbox and privileged environment

ChromeRPC

I will eventually turn this into a bower module when I have time with tests and the whole shebang.

Usage

background.html

<html>
<head>
<script src='background.js'/>
</head>
<body>
<iframe id='iframe' src='main.js'/>
</body>
</html>

background.js

iframe = document.getElementById 'iframe'
rpc = ChromeRPC.PackagedAppRPC iframe.contentWindow
rpc.on 'getFoo', (data, cb) -> 
  if data.foo
    cb null, {foo: 'bar'}

main.coffee

rpc = ChromeRPC.PackagedAppRPC window.top

rpc.call 'getFoo', {foo: true}, (err, res) ->
  if err then return console.error err
  console.log 'Got a response:', res
@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
@burkeholland
Copy link

That's HAWT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment