Tab Completion

I'm Tab Atkins Jr, and I wear many hats. I work for Google on the Chrome browser as a Web Standards Hacker. I'm also a member of the CSS Working Group, and am either a member or contributor to several other working groups in the W3C. You can contact me here.
Listing of All Posts

Delegating to a Possibly-Existing Object in JS

Last updated:

Earlier today Hixie got himself confused trying to write some new DOM API in terms of Proxies. I claimed it was easy and that I could do it for him. Here's his original request:

"i have JS objects that represent objects in a server-side data structure, which is lazily loaded as needed. I don't know the types of the objects until I get them, but I need to instantiate the objects before I get them. The API to those objects is all async (callback-based)."

Requirements

  1. An author calls some API and gets an object back immediately. This object "represents" some server-side object. The API call fires off an XHR to request the server data.
  2. The object has some properties that are always available, representing some sort of related information that isn't stored on the server.
  3. The object has an async api for getting at the server data. Until the XHR returns, any requests are queued. Afterwards, they're simply forwarded.

Code

var FarObject = 
(function(){
  var far = new Name();
  var queue = new Name();

  return function () {
    this[queue] = [];

    doXHR('url', {success: data => {
      // Based on the returned data,
      // create any of several far objects,
      // with different APIs.
      this[far] = makeSpecificFarObject(data);
      this[queue].forEach(f => this[far][f.name](...f.args));
      delete this[queue];
    }});

    // The near object contains attributes and functions
    // that are always/immediately available,
    // regardless of whether the XHR is finished or not.
    var near = makeNearObject();

    return Proxy(near, {
      get: (near, name, receiver) => {
        if( near[name] ) return Reflect.get(near, name, receiver);
        return (...args) => { 
          if( this[far] )
            this[far][name](...args);
          else
            this[queue].push({name, args});
        };
      }
    })
  }
})();

Note that this uses Names (necessary for efficient private vars), the spread operator (optional, but makes it easier to read), shorthand object initializers (same), and fat arrows (same again). I also use an imaginary better version of XHR because, seriously, fuck XHR.

After you figure out what kind of far object you're creating (based on the returned data), you probably want to add the appropriate functions to the "near" object, so that they'll show up as existing according to all the other proxy traps that forward to "near". Then you just need to add a little bit more logic to the "get" trap, replacing "if( near[name] )" with something that only passes with the "original" properties on the near object, so the new functions will get passed onto the far object as appropriate.

(a limited set of Markdown is supported)