         return reply
     }
     
-    public func publishEvent(eventName: String, args: AnyObject) {
+    /// Call an action over the JSBridge and execute a callback with the result. Invokation of the callback is
+    /// not guaranteed, for example if the function is never registered. Unlike the synchronous version, this
+    /// function does not return an error for unregistered functions, instead caching the args and re-triggering
+    /// the action once registration occurs.
+    ///
+    /// - parameter functionName: String name of the function to call
+    /// - parameter args:         arguments to pass to the function
+    /// - parameter callback:     Callback to invoke with the result
+    public func callAction(functionName: String, args: Array<AnyObject>, callback: Callback) {
+        guard self.registeredActions.contains(functionName) else {
+            dispatch_sync(lockDispatchQueue) {
+                if self.awaitingRegistration[functionName] == nil {
+                    self.awaitingRegistration[functionName] = []
+                }
+                self.awaitingRegistration[functionName]! += [(args, callback)]
+            }
+            return
+        }
+        
+        let actionId = nextActionId()
+        
+        dispatch_sync(semaphoresDispatchQueue) {
+            self.eventCallbacks[actionId] = callback
+        }
+        // only send event - callback is handled in action reply
+        self.sendEventWithName("callAction", body: ["id": actionId, "action": functionName, "args": args])
+        
+    }
+    
+    /// Publish an event to Javascript over the JSBridge
+    ///
+    /// - parameter eventName: String name of event
+    /// - parameter args:      Array args to pass with the event
+    public func publishEvent(eventName: String, args: Array<AnyObject>) {
         self.sendEventWithName("publishEvent", body: ["event": eventName, "args": args])
     }
     
     @objc(replyToAction:response:error:)
-    func replyToAction(actionId: NSInteger, response: NSDictionary, error: String) {
-//        print("Response \(actionId)")
-        
+    func replyToAction(actionId: NSInteger, response: NSObject, error: String) {
+        let result = ["result": response, "error": error]
         dispatch_async(semaphoresDispatchQueue) {
-            // lookup the semaphore for this action
+            // we should find either a semaphore or a callback for this action
             if let sem = self.eventSemaphores[actionId] {
                 // place response in cache and signal on this semaphore
-                self.replyCache[actionId] = ["result": response, "error": error]
+                self.replyCache[actionId] = result
                 dispatch_semaphore_signal(sem)
-            } else {
-                print("disgarding action reply \(actionId)")
+            } else if let callback = self.eventCallbacks[actionId] {
+                dispatch_async(self.callbackDispatchQueue) {
+                    callback(result)
+                }
+                self.eventCallbacks[actionId] = nil
             }
         }
     }

@objc(JSBridge)
public class JSBridge : RCTEventEmitter {

    var registeredActions: Set<String> = []
    var actionCounter: NSInteger = 0
    var replyCache = [NSInteger: [String: NSObject]]()
    var eventSemaphores = [NSInteger: dispatch_semaphore_t]()
    private let semaphoresDispatchQueue = dispatch_queue_create("com.cliqz.jsbridge.sync", DISPATCH_QUEUE_CONCURRENT)
    let ACTION_TIMEOUT : Int64 = 200000000 // 200ms
    
    public override init() {

    @objc(registerAction:)
    func registerAction(actionName: String) {
        self.registeredActions.insert(actionName)
        
        // check for actions waiting to be executed
        dispatch_async(lockDispatchQueue) {
            if let queued = self.awaitingRegistration[actionName] {
                // trigger each waiting action
                queued.forEach { (args, callback) in
                    dispatch_async(self.callbackDispatchQueue, {
                        self.callAction(actionName, args: args, callback: callback)
                    })
                }
                self.awaitingRegistration[actionName] = []
            }
        }
    }
    
}
