meta-objects

Utilities and patterns for using Harmony Proxies to meta-program. Proxies as Proxy handlers, catch all forwarders for catch all forwarders, intercession introspection. Now you're thinking with portals.

npm install meta-objects
19 downloads in the last week
75 downloads in the last month

Meta-Objects

Tools for making meta-objects. These are broken down into two categories. One category is lower level tools that would be used for library authors, people making virtual objects, etc. The other is tools built on top of those, that are ready to use out of the box for easy usage.

Easy to use interfaces

  • callable - Takes an object and returns a callable and constructable version of it, which use the $$call and $$construct properties on the object.
  • multiherit - Creates a meta class from multiple constructors that combines all of them into one. Prototypal inheritance with multiple prototypes and multiple constructors.
  • tracer - Creates a clone object from the provided object that emits all things that happen to it. It's like databinding on crack.
  • interceptor - Turns normal getters/setters on prototypes, get/set/length into indexed properties like buffers, and get/set/has/list into named properties like DOM css objects.

  • proxy - Simplifies creation of ES6 proxies by handling everything you don't want to and providing ways to forward and subtly modify a target object.

  • membrane - Accepts the same parameters that would be provided to proxy but returns an object that will automatically wrap and unwrap all sub-objects gotten from property access, function calling, constructors, etc.
  • deppelgangar - An object with mutatable persona. That is, it has a single JavaScript identity (used in === for example) but allows you to arbitrarily reassign what object it mirrors, at any time.
  • WrapMap - Mostly internal use tool for wrapping/unwrapping objects, used by membranes.
  • Emitter - A cross-between Node's emitters and the DOM event system. Used by tracer.

Install

This library relies on features from the next version of ECMAScript and isn't going to work in most environments.

WeakMap and Proxy are required. This library works in:

  • Firefox 4+
  • Chrome 18+ with the experimental javascript flag enabled
  • Node 0.7+ when run like so node --harmony

Browser

<script src="meta-objects.browser.min.js"></script>
<scrript>console.log(meta)</scrript>

Node.js

npm install meta-objects
var meta = require('meta-objects');
console.log(meta);

Interceptors

The interceptor function turns an existing Constructor and its prototype into a more versatile interface, depending on what methods you provide on the prototype. For any properties that aren't intercepted by the below methods the properties will simply work like normal.

All of the below are created by just calling meta.interceptor(Constructor). The interface bestowed on your prototype will base based on the methods available on it.

  • Interceptor - is the baseline. It simply uses any getter/setters on your prototype and shows instances as having them as own regular properties.
  • IndexedInterceptor - If your prootype has length, get, and (optionally) set functions then the interface will additionally show indexed properties as own normal properties, utilizing those functions to fulfill interaction with numbered properties. This allows you to implement objects with buffer type interfaces that previously would be done in C++.
  • NamedInterceptor - If you provide a list and has function instead of length (along with get/optional set) your class will be be upgraded to an interface that provides those properties as named own normal properties.

You will receive a new constructor with a new prototype that will denend on the originally provided ones in order to work, but any objects created will be instances of the new prototype and the new prototype will show have the new constructor as its constructor.

Doppelganger

The doppelganger is a simple interface. Calling meta.doppelganger(callable?) will return a function that changes the identity of your new doppelgagner instance, and also returns it. Whether an object is callable as a function is not mutable after creation, so you have to decide up front. A callable one will be typeof === 'function'.

var changeInto = meta.doppelganger();

// the first call actually provides the object to you
var doppel = changeInto(document);
console.log(doppel.location);

changeInto([1,2, 3]);
console.log(doppel.length);

## Callable Objects
A simple to use function that takes an object and returns a mirror of it that is callable and constructable. All changes to one reflect the other so they are in effect the same object, except the returned version is a function and they are not `===`.

__callable(obj)__

* __obj.$$call__ - If present, this function will be called when the callable object is in any manner. `obj()` or `obj.call(x, args...)`. `this` is bound to the object itself so using call, bind, or apply won't have any effect on `this`.
* __obj.$$construct__ - If preset, this function will be called whenever the object is constructed using `new obj(...args)`. `this` will be a newly created object as if created using `Object.create(obj)` but it will also be callable and not just an object.

If `obj.$$call` is not present then the call will simply do nothing. If `obj.$$construct` is not present, then the newly created callable child of the object will be returned as is, so it's still useful for creating new instances without any constructor logic.

`obj.$$call` and `obj.$$construct` are completely invisible properties but (currently) are still gettable and settable. It's likely this will be changed so there's some specific procedure for assessing these essentially private properties.


```javascript
var obj = {
  y: 10,
  $$call: function(){
    return this.y++;
  }
};
var cobj = callable(obj);

console.log(obj);
//-->
  { y: 10, $$call: [Function] }

console.log(cobj);
//-->
  { [Function] y: 10 }

cobj();
console.log(cobj);
//-->
  { [Function] y: 11 }

Multiple Inheritance

The multiherit function provided allows for simple creation of classes that inherit from multiple objects. That is, you provide a set of constructors (and implicitly the .prototype for each) and in return you receive a single constructor with a single prototype. The constructor invokes all of the provided constructors in the order provided. The prototype combines all of the properties of the prototypes, in preference to the order provided.

multiherit(options)

  • options.ctors is an array of constructors that will be composed. The prototype property will be used for determining the list of prototypes.
  • options.params(strings) is an array of names that will be used to map params given to the combined ctor to each of the individual ctors. The names are matched to the named parameters of each ctor.
  • options.params(number arrays) can be used instead to map the parameters by input order to the matching index ctor.
  • options.name optionally sets a specific name for the resulting Ctor. If not provided it will be made from combining the names of the given ctors.
  • options.onCall optionally make instances callable and provide the function to call when they are called
  • options.onConstruct optionally make instances constructable and provide the constructor function
function Talks(name, says){
  this.name = name;
  this.says = says;
}

Talks.prototype = {
  speak: function speak(at){
    at(this.name + ' says: ' + this.says);
  }
};

function Walks(name, stride){
  this.name = name;
  this.stride = stride;
}

Walks.prototype = {
  move: function move(where){
    var self = this;
    setTimeout(function(){
      where[self.name] = self;
      self.location = where;
    }, this.stride);
  }
}

function Fondles(desires){
  this.desires = desires;
}

Fondles.prototype = {
  touch: function touch(who){
    this.felt = who[this.desires];
  }
};

// parameter names are used to map the combined ctor's parameters to the set of constructors
var WalksTalksFondles = multiherit({
  ctors: [Walks, Talks, Fondles],
  params: ['name', 'says', 'stride', 'desires']
});
console.log(WalksTalksFondles)
//-->
  [Function: WalksTalksFondles]

// the combined prototype can also have its own properties separate from any it inherits
WalksTalksFondles.prototype.type = 'man';

// the inheritance is dynamic, just like normal [[prototypes]]
Walks.prototype.speed = 1000;

console.log(WalksTalksFondles.prototype)
//-->
  { type: 'man',
    move: [Function: move],
    speed: 1000,
    speak: [Function: speak],
    touch: [Function: touch] }


var bob = new WalksTalksFondles('bob', 'hey guys', 100, 'name');
bob.touch(Object)
bob.speak(console.log); //bob says: hey guys
console.log(bob);
//-->
  { name: 'bob',
    stride: 100,
    says: 'hey guys',
    desires: 'name',
    felt: 'Object' }

console.log(bob.constructor)
//-->
  [Function: WalksTalksFondles]

Tracer Objects

The tracer function takes any object and produces an event emitter along with a clone of the object. All interactions with the cloned version will be broadcast from the emitter. The clone can be interacted with in any way as if it were the real object. Any sub-object gotten from the clone through property access, function calling, constructors, etc. will also be a clone that broadcasts to the emitter.

var emitter = meta.tracer(function Test(){ this.name = 'bob' }, 'Test');
emitter.on('*', function(event){
  if (event.property) {
    console.log(event.type, event.path.join('.'), event.property);
  } else {
    console.log(event.type, event.path.join('.'));
  }
});

var Test = emitter.Test;
var bob = new Test;

console.log(bob);
//-->
  construct Test
  get Test.[[0]] inspect  // instances created via construct are indexed and displayed as [[#]]
  keys Test.[[0]]
  describe Test.[[0]] name
  { name: 'bob' }

bob.stuff = { x: ['a',',b','c'] }
console.log(bob.stuff.x[0]);

//-->
  set Test.[[0]] stuff
  get Test.[[0]] stuff
  get Test.[[0]].stuff x
  get Test.[[0]].stuff.x 0
  a

proxy

A easier to use proxy that's closer to the new Proxy spec than the old one, but with some niceities to make proxy handler creation much simpler. When creating a proxy you provide the object to mirror and the handler. The default action is to forward all actions to the provided target, no traps need be implemented at all.

var proxied = meta.proxy(target, handler);

Traps Some traps have been renamed because I hate long names. Parameters have been reordered for convenience. The first parameter for all traps is fwd, a function that will forward the event to the target. It takes no arguments but has three properties which can be modified. fwd can be invoked multiple times, allowing you to, for example, mirror the action against two separate targets.

  • fwd.target can be changed to forward the event to something else.
  • fwd.args is an array with the arguments starting after target. For example, to change the property for traps against a specific property, you would do fwd.args[0] = 'differentProperty'; fwd();
  • fwd.trap The string name of the trap. Changing this invokes a different forwarding action. For example, in the keys trap you could change it to names which would then show hidden properties instead of non-hidden ones.
keys      [fwd, target]
names     [fwd, target]
enumerate [fwd, target]
fix       [fwd, target]
owns      [fwd, target, prop]
has       [fwd, target, prop]
delete    [fwd, target, prop]
describe  [fwd, target, prop]
define    [fwd, target, prop, descriptor]
get       [fwd, target, prop, receiver]
set       [fwd, target, prop, value, receiver]
apply     [fwd, target, args, receiver]
construct [fwd, target, args]

membrane

Takes a proxy handler designed for the above proxy handler api and makes it into a membrane that automatically wraps all outbound objects and unwraps all inbound objects. The handlers only see unwrapped normal objects so it's no more complicated than creating a normal proxy.

Emitter

An object that's cross between DOM events and Node's EventEmitter. Event objects are either used or created which include information about the current target and type of event. Handlers and meta information aren't stored on the emitter itself which allows for separation of the event process and an objects normal operation. An emitter can "forward" all event subscriptions from itself to another object such that doing obj.on('event', callback) subscribes to events emitted by obj2.

The this binding for all callbacks is the emitter of the event, and the first parameter is and event object which includes the target of the event. This is usually the same as the emitter, but in some cases it may be different. If events are forwarded to another object, or if you're listening to a prototype and getting all events for every instance inheriting from it, the this binding will be the object you registered with using emitter.on, and the target will be the object responsible for emitting the event. This is similar to how most DOM frameworks work with event delegation and dispatching.

WrapMap

A utility class for simplifying usage of creating wrapper/unwrappers that use WeakMaps. Simply provide a callback function that accepts an input unwrapped object and returns a wrapped version. The result is a WrapMap instance with the following functions

  • wrap - Wraps objects if they aren't already wrapped. Returns primitives and wrapped objects unmodified.
  • unwrap - Unwraps wrapped objects and returns primitives and unwrapped objects unmodified.
  • wrapDescriptor - Applies the wrapper to obj.value, obj.set, and obj.set but not the base object itself. Modifies the descriptor.
  • unwrapDescriptor - Unwraps value, set, and get. Modifies the descriptor.
  • remove - Unwrap object and remove from the map, returning the unwrapped object.
  • has - Boolean testing if a given object is wrapped.
var arrayer = new WrapMap(function(obj){
  return [];
});

var test = arrayer.wrap(function test(){});
console.log(test);
//-->
  []

console.log(arrayer.unwrap(test));
//-->
  [Function: test];
npm loves you