Index: chrome/ext/background.js
===================================================================
--- a/chrome/ext/background.js
+++ b/chrome/ext/background.js
@@ -17,187 +17,111 @@
 
 (function()
 {
-  /* Events */
+  /* Pages */
 
-  var TabEventTarget = function()
+  var sendMessage = chrome.tabs.sendMessage || chrome.tabs.sendRequest;
+
+  var Page = ext.Page = function(tab)
   {
-    WrappedEventTarget.apply(this, arguments);
+    this._id = tab.id;
+    this._url = tab.url;
 
-    this._tabs = {};
+    this.browserAction = new BrowserAction(tab.id);
+  };
+  Page.prototype = {
+    get url()
+    {
+      // usually our Page objects are created from Chrome's Tab objects, which
+      // provide the url. So we can return the url given in the constructor.
+      if (this._url != null)
+        return this._url;
 
-    this._sharedListener = this._sharedListener.bind(this);
-    this._removeTab = this._removeTab.bind(this);
-  };
-  TabEventTarget.prototype = {
-    __proto__: WrappedEventTarget.prototype,
-    _bindToTab: function(tab)
+      // but sometimes we only have the tab id when we create a Page object.
+      // In that case we get the url from top frame of the tab, recorded by
+      // the onBeforeRequest handler.
+      var frames = framesOfTabs[this._id];
+      if (frames)
+      {
+        var frame = frames[0];
+        if (frame)
+          return frame.url;
+      }
+    },
+    activate: function()
     {
-      return {
-        addListener: function(listener)
-        {
-          var listeners = this._tabs[tab._id];
-
-          if (!listeners)
-          {
-            this._tabs[tab._id] = listeners = [];
-
-            if (Object.keys(this._tabs).length == 1)
-              this.addListener(this._sharedListener);
-
-            tab.onRemoved.addListener(this._removeTab);
-          }
-
-          listeners.push(listener);
-        }.bind(this),
-        removeListener: function(listener)
-        {
-          var listeners = this._tabs[tab._id];
-          if (!listeners)
-            return;
-
-          var idx = listeners.indexOf(listener);
-          if (idx == -1)
-            return;
-
-          listeners.splice(idx, 1);
-
-          if (listeners.length == 0)
-            tab.onRemoved.removeListener(this._removeTab);
-          else
-          {
-            if (listeners.length > 1)
-              return;
-            if (listeners[0] != this._removeTab)
-              return;
-          }
-
-          this._removeTab(tab);
-        }.bind(this)
-      };
+      chrome.tabs.update(this._id, {selected: true});
     },
-    _sharedListener: function(tab)
+    sendMessage: function(message, responseCallback)
     {
-      var listeners = this._tabs[tab._id];
-
-      if (!listeners)
-        return;
-
-      // copy listeners before calling them, because they might
-      // add or remove other listeners, which must not be taken
-      // into account before the next occurrence of the event
-      listeners = listeners.slice(0);
-
-      for (var i = 0; i < listeners.length; i++)
-        listeners[i](tab);
-    },
-    _removeTab: function(tab)
-    {
-      delete this._tabs[tab._id];
-
-      if (Object.keys(this._tabs).length == 0)
-        this.removeListener(this._sharedListener);
+      sendMessage(this._id, message, responseCallback);
     }
   };
 
-  var BeforeNavigateTabEventTarget = function()
-  {
-    TabEventTarget.call(this, chrome.webNavigation.onBeforeNavigate);
-  };
-  BeforeNavigateTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
+  ext.pages = {
+    open: function(url, callback)
     {
-      return function(details)
+      if (callback)
       {
-        if (details.frameId == 0)
-          listener(new Tab({id: details.tabId, url: details.url}));
-      };
-    }
+        chrome.tabs.create({url: url}, function(openedTab)
+        {
+          var onUpdated = function(tabId, changeInfo, tab)
+          {
+            if (tabId == openedTab.id && changeInfo.status == "complete")
+            {
+              chrome.tabs.onUpdated.removeListener(onUpdated);
+              callback(new Page(tab));
+            }
+          };
+          chrome.tabs.onUpdated.addListener(onUpdated);
+        });
+      }
+      else
+        chrome.tabs.create({url: url});
+    },
+    query: function(info, callback)
+    {
+      var rawInfo = {};
+      for (var property in info)
+      {
+        switch (property)
+        {
+          case "active":
+          case "lastFocusedWindow":
+            rawInfo[property] = info[property];
+        }
+      }
+
+      chrome.tabs.query(rawInfo, function(tabs)
+      {
+        callback(tabs.map(function(tab)
+        {
+          return new Page(tab);
+        }));
+      });
+    },
+    onLoading: new ext._EventTarget()
   };
 
-  var LoadingTabEventTarget = function()
+  chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab)
   {
-    TabEventTarget.call(this, chrome.tabs.onUpdated);
-  };
-  LoadingTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(id, info, tab)
-      {
-        if (info.status == "loading")
-          listener(new Tab(tab));
-      };
-    }
-  };
+    if (changeInfo.status == "loading")
+      ext.pages.onLoading._dispatch(new Page(tab));
+  });
 
-  var CompletedTabEventTarget = function()
+  chrome.webNavigation.onBeforeNavigate.addListener(function(details)
   {
-    TabEventTarget.call(this, chrome.tabs.onUpdated);
-  };
-  CompletedTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(id, info, tab)
-      {
-        if (info.status == "complete")
-          listener(new Tab(tab));
-      };
-    }
-  };
+    if (details.frameId == 0)
+      ext._removeFromAllPageMaps(details.tabId);
+  });
 
-  var ActivatedTabEventTarget = function()
+  chrome.tabs.onRemoved.addListener(function(tabId)
   {
-    TabEventTarget.call(this, chrome.tabs.onActivated);
-  };
-  ActivatedTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(info)
-      {
-        chrome.tabs.get(info.tabId, function(tab)
-        {
-          listener(new Tab(tab));
-        });
-      };
-    }
-  }
+    ext._removeFromAllPageMaps(tabId);
+    delete framesOfTabs[tabId];
+  });
 
-  var RemovedTabEventTarget = function()
-  {
-    TabEventTarget.call(this, chrome.tabs.onRemoved);
-  };
-  RemovedTabEventTarget.prototype = {
-    __proto__: TabEventTarget.prototype,
-    _wrapListener: function(listener)
-    {
-      return function(id) { listener(new Tab({id: id})); };
-    }
-  };
 
-  var BackgroundMessageEventTarget = function()
-  {
-    MessageEventTarget.call(this);
-  }
-  BackgroundMessageEventTarget.prototype = {
-    __proto__: MessageEventTarget.prototype,
-    _wrapSender: function(sender)
-    {
-      var tab = new Tab(sender.tab);
-      
-      //url parameter is missing in sender object (Chrome v28 and below)
-      if (!("url" in sender))
-        sender.url = tab.url;
-      return {tab: tab, frame: new Frame({url: sender.url, tab: tab})};
-    }
-  };
-
-
-  /* Tabs */
-
-  var sendMessage = chrome.tabs.sendMessage || chrome.tabs.sendRequest;
+  /* Browser actions */
 
   var BrowserAction = function(tabId)
   {
@@ -244,148 +168,15 @@
     }
   };
 
-  Tab = function(tab)
-  {
-    this._id = tab.id;
-    this._url = tab.url;
-
-    this.browserAction = new BrowserAction(tab.id);
-
-    this.onLoading = ext.tabs.onLoading._bindToTab(this);
-    this.onCompleted = ext.tabs.onCompleted._bindToTab(this);
-    this.onActivated = ext.tabs.onActivated._bindToTab(this);
-    this.onRemoved = ext.tabs.onRemoved._bindToTab(this);
-
-    // the "beforeNavigate" event in Safari isn't dispatched when a new URL
-    // was entered into the address bar. So we can only use it only on Chrome,
-    // but we have to hide it from the browser-independent high level code.
-    this._onBeforeNavigate = ext.tabs._onBeforeNavigate._bindToTab(this);
-  };
-  Tab.prototype = {
-    get url()
-    {
-      // usually our Tab objects are created from chrome Tab objects, which
-      // provide the url. So we can return the url given in the constructor.
-      if (this._url != null)
-        return this._url;
-
-      // but sometimes we only have the id when we create a Tab object.
-      // In that case we get the url from top frame of the tab, recorded by
-      // the onBeforeRequest handler.
-      var frames = framesOfTabs.get(this);
-      if (frames)
-      {
-        var frame = frames[0];
-        if (frame)
-          return frame.url;
-      }
-    },
-    close: function()
-    {
-      chrome.tabs.remove(this._id);
-    },
-    activate: function()
-    {
-      chrome.tabs.update(this._id, {selected: true});
-    },
-    sendMessage: function(message, responseCallback)
-    {
-      sendMessage(this._id, message, responseCallback);
-    }
-  };
-
-  TabMap = function(deleteOnPageUnload)
-  {
-    this._map = {};
-
-    this._delete = this._delete.bind(this);
-    this._deleteOnPageUnload = deleteOnPageUnload;
-  };
-  TabMap.prototype = {
-    get: function(tab)
-    {
-      return (this._map[tab._id] || {}).value;
-    },
-    set: function(tab, value)
-    {
-      if (!(tab._id in this._map))
-      {
-        tab.onRemoved.addListener(this._delete);
-        if (this._deleteOnPageUnload)
-          tab._onBeforeNavigate.addListener(this._delete);
-      }
-
-      this._map[tab._id] = {tab: tab, value: value};
-    },
-    has: function(tab)
-    {
-      return tab._id in this._map;
-    },
-    clear: function()
-    {
-      for (var id in this._map)
-        this.delete(this._map[id].tab);
-    },
-    _delete: function(tab)
-    {
-      // delay so that other event handlers can still lookup this tab
-      setTimeout(this.delete.bind(this, tab), 0);
-    },
-    delete: function(tab)
-    {
-      delete this._map[tab._id];
-
-      tab.onRemoved.removeListener(this._delete);
-      tab._onBeforeNavigate.removeListener(this._delete);
-    }
-  };
-
-
-  /* Windows */
-
-  Window = function(win)
-  {
-    this._id = win.id;
-    this.visible = win.status != "minimized";
-  };
-  Window.prototype = {
-    getAllTabs: function(callback)
-    {
-      chrome.tabs.query({windowId: this._id}, function(tabs)
-      {
-        callback(tabs.map(function(tab) { return new Tab(tab); }));
-      });
-    },
-    getActiveTab: function(callback)
-    {
-      chrome.tabs.query({windowId: this._id, active: true}, function(tabs)
-      {
-        callback(new Tab(tabs[0]));
-      });
-    },
-    openTab: function(url, callback)
-    {
-      var props = {windowId: this._id, url: url};
-
-      if (!callback)
-        chrome.tabs.create(props);
-      else
-        chrome.tabs.create(props, function(tab)
-        {
-          callback(new Tab(tab));
-        });
-    }
-  };
-
 
   /* Frames */
 
-  var framesOfTabs = new TabMap();
+  var framesOfTabs = {__proto__: null};
 
-  Frame = function(params)
+  var Frame = ext.Frame = function(params)
   {
-    this._tab = params.tab;
-    this._id = params.id;
+    this._frameId = params.frameId;
+    this._tabId = params.tabId;
     this._url = params.url;
   };
   Frame.prototype = {
@@ -394,22 +185,22 @@
       if (this._url != null)
         return this._url;
 
-      var frames = framesOfTabs.get(this._tab);
+      var frames = framesOfTabs[this._tabId];
       if (frames)
       {
-        var frame = frames[this._id];
+        var frame = frames[this._frameId];
         if (frame)
           return frame.url;
       }
     },
     get parent()
     {
-      var frames = framesOfTabs.get(this._tab);
+      var frames = framesOfTabs[this._tabId];
       if (frames)
       {
         var frame;
-        if (this._id != null)
-          frame = frames[this._id];
+        if (this._frameId != null)
+          frame = frames[this._frameId];
         else
         {
           // the frame ID wasn't available when we created
@@ -428,13 +219,18 @@
         if (!frame || frame.parent == -1)
           return null;
 
-        return new Frame({id: frame.parent, tab: this._tab});
+        return new Frame({frameId: frame.parent, tabId: this._tabId});
       }
     }
   };
 
 
-  /* Web request blocking */
+  /* Web requests */
+
+  ext.webRequest = {
+    onBeforeRequest: new ext._EventTarget(true),
+    handlerBehaviorChanged: chrome.webRequest.handlerBehaviorChanged
+  };
 
   chrome.webRequest.onBeforeRequest.addListener(function(details)
   {
@@ -446,13 +242,12 @@
       if (details.tabId == -1)
         return;
 
-      var tab = new Tab({id: details.tabId});
-      var frames = framesOfTabs.get(tab);
+      var page = new Tab({id: details.tabId});
+      var frames = framesOfTabs[details.tabId];
 
       if (!frames)
       {
-        frames = [];
-        framesOfTabs.set(tab, frames);
+        frames = framesOfTabs[details.tabId] = [];
 
         // assume that the first request belongs to the top frame. Chrome
         // may give the top frame the type "object" instead of "main_frame".
@@ -493,13 +288,10 @@
           frames[details.frameId].parent = frameId;
       }
 
-      var frame = new Frame({id: frameId, tab: tab});
+      var frame = new Frame({id: frameId, tabId: details.tabId});
 
-      for (var i = 0; i < ext.webRequest.onBeforeRequest._listeners.length; i++)
-      {
-        if (ext.webRequest.onBeforeRequest._listeners[i](details.url, details.type, tab, frame) === false)
-          return {cancel: true};
-      }
+      if (!ext.webRequest.onBeforeRequest._dispatch(details.url, details.type, page, frame))
+        return {cancel: true};
     }
     catch (e)
     {
@@ -511,49 +303,11 @@
   }, {urls: ["<all_urls>"]}, ["blocking"]);
 
 
-  /* API */
-
-  ext.windows = {
-    getAll: function(callback)
-    {
-      chrome.windows.getAll(function(windows)
-      {
-        callback(windows.map(function(win)
-        {
-          return new Window(win);
-        }));
-      });
-    },
-    getLastFocused: function(callback)
-    {
-      chrome.windows.getLastFocused(function(win)
-      {
-        callback(new Window(win));
-      });
-    }
-  };
-
-  ext.tabs = {
-    onLoading: new LoadingTabEventTarget(),
-    onCompleted: new CompletedTabEventTarget(),
-    onActivated: new ActivatedTabEventTarget(),
-    onRemoved: new RemovedTabEventTarget(),
-
-    // the "beforeNavigate" event in Safari isn't dispatched when a new URL
-    // was entered into the address bar. So we can only use it only on Chrome,
-    // but we have to hide it from the browser-independent high level code.
-    _onBeforeNavigate: new BeforeNavigateTabEventTarget()
-  };
-
-  ext.webRequest = {
-    onBeforeRequest: new SimpleEventTarget(),
-    handlerBehaviorChanged: chrome.webRequest.handlerBehaviorChanged
-  };
-
-  ext.storage = localStorage;
+  /* Context menus */
 
   var contextMenuItems = [];
   var isContextMenuHidden = true;
+
   ext.contextMenus = {
     addMenuItem: function(title, contexts, onclick)
     {
@@ -562,7 +316,7 @@
         contexts: contexts,
         onclick: function(info, tab)
         {
-          onclick(info.srcUrl, new Tab(tab));
+          onclick(info.srcUrl, new Page(tab));
         }
       });
       this.showMenuItems();
@@ -601,5 +355,19 @@
     }
   };
 
-  ext.onMessage = new BackgroundMessageEventTarget();
+
+  /* Message passing */
+
+  ext._setupMessageListener(function(sender)
+  {
+    return {
+      page: new Page(sender.tab),
+      frame: new Frame({url: sender.url, tabId: sender.tab.id})
+    };
+  });
+
+
+  /* Storage */
+
+  ext.storage = localStorage;
 })();
