metarhia / jstp

Fast RPC for browser and Node.js based on TCP, WebSocket, and MDSF
https://metarhia.github.io/jstp
Other
142 stars 10 forks source link

Implement JSTP Record Data and JSTP Record Metadata #19

Open aqrln opened 7 years ago

aqrln commented 7 years ago

Conceptual code by @tshemsedinov copied from Impress:

// Assign metadata to array elements
//   data - array of objects serialized into arrays with JSTP single object
//   metadata - data describes PrototypeClass structure
// Returns: built PrototypeClass
//
api.jstp.assignMetadata = function(data, metadata) {
  var proto = api.jstp.buildPrototype(metadata);
  api.jstp.assignPrototype(data, proto);
  return proto;
};

// Assign prototype to records array or single record
//   data - array of objects serialized into arrays with JSTP single object
//   proto - dynamically built prototype to be assigned
//
api.jstp.assignPrototype = function(data, proto) {
  if (Array.isArray(data)) {
    data.forEach(function(item) {
      item.__proto__ = proto.prototype;
    });
  } else {
    data.__proto__ = proto.prototype;
  }
};

// Build Prototype from Metadata
//
api.jstp.buildPrototype = function(metadata) {
  var protoClass = function ProtoClass() {};
  var index = 0, fieldDef, buildGetter, fieldType;
  for (var name in metadata) {
    fieldDef = metadata[name];
    fieldType = typeof(fieldDef);
    if (fieldType !== 'function') fieldType = fieldDef;
    buildGetter = api.jstp.accessors[fieldType];
    if (buildGetter) buildGetter(protoClass, name, index++, fieldDef);
  }
  return protoClass;
};

api.jstp.accessors = {};

api.jstp.accessors.string = function(proto, name, index) {
  Object.defineProperty(proto.prototype, name, {
    get: function() {
      return this[index];
    },
    set: function(value) {
      this[index] = value;
    }
  });
};

api.jstp.accessors.Date = function(proto, name, index) {
  Object.defineProperty(proto.prototype, name, {
    get: function() {
      return new Date(this[index]);
    },
    set: function(value) {
      this[index] = value instanceof Date ? value.toISOString() : value;
    }
  });
};

api.jstp.accessors.function = function(proto, name, index, fieldDef) {
  Object.defineProperty(proto.prototype, name, { get: fieldDef });
};
aqrln commented 7 years ago

The solution with assigning a dynamically generated prototype to an array is elegant but I see several problems here:

  1. Performance. Changing the prototype of an existing object is not only quite a slow operation itself but also can cause deopts in the code that references such objects.

    Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-flung, and are not limited to simply the time spent in obj.__proto__ = ... statement, but may extend to any code that has access to any object whose [[Prototype]] has been altered. If you care about performance you should avoid setting the [[Prototype]] of an object.

    (source)

    This can be worked around, though, by constructing a new object using Object.create(newPrototype, oldObject). But after this operation JSRD array will definitely not be represented as an array internally. Instead, it will be a hash map with numeric keys, in addition to string keys defined in prototype, so we get an extra overhead.

  2. We lose some flexibility when working with such objects because it would be necessary to treat them specifically. Not much of JavaScript code expects any properties except for functions to be inherited via prototype chain so any function that relies on Object.keys() or hasOwnProperty() will be broken when such object is passed. For example, if we needed to iterate such object's keys, we would either need to use a slow for-in loop, or pass metadata along with the object, iterate its keys and check for corner cases such as optional keys manually.

To conclude, it will be better to construct an object instead of assigning a prototype. Such objects will not have subtle drawbacks when working with them and, even more importantly, will work significantly faster.

FYI @tshemsedinov

aqrln commented 7 years ago

We will need custom prototypes, though. For functions.

Thus the basic algorithm is:

  1. If the prototype is not found in registry, create a prototype containing all the functions defined in metadata and cache it for later usage with all objects of that type.
  2. Create an empty object using Object.create(prototype).
  3. For each property declared in metadata, take the next value from record data and assign the property to the object.