"use strict"; const mapObj = require("map-obj"); const typeRules = require("./type-rules"); const generateDescriptor = require("./generate-descriptor"); const getSchemaKeys = require("./util/get-schema-keys"); const nullMissingFields = require("./util/null-missing-fields"); const generateTypeHash = require("./type-hash"); const serialize = require("./serialization/serialize"); const deserialize = require("./serialization/deserialize"); let registeredTypes = new Set(); let registeredTypesDirty = false; let typeMap = new Map(); function hashAllTypes() { if (registeredTypesDirty) { registeredTypesDirty = false; for (let type of registeredTypes) { type._ensureHash(); } } } module.exports = function createType(name, schema) { if (schema._isTypeRule === true) { return typeRules._createTypeRule({ _typeName: name, _isTypeAlias: true, _alias: schema }); } else { let propertyDescriptors = mapObj(schema, (key, rule) => { return generateDescriptor(rule, key, false); }); let protoProperties = { _typeName: name, /* We pre-define this here so that instances can be monomorphic; this provides a 4x performance improvement. */ _data: mapObj(schema, (key, _value) => { return [key, undefined]; }), /* FIXME: Disallow overriding this. */ serialize: function (serializer) { return serialize(this, { serializer: serializer }); } }; let proto = Object.create(protoProperties, propertyDescriptors); let schemaKeys = getSchemaKeys(schema); let factory = function createInstance(instanceProps) { factory._isUsed = true; let instance = Object.create(proto); instance._data = {}; // for (let key of factory._schemaKeys) { // if (instanceProps[key] != null) { // instance[key] = instanceProps[key]; // } else { // instance[key] = null; // } // } /* FIXME: Investigate ways to optimize this */ let nullFields = nullMissingFields(instanceProps, factory._schemaKeys); Object.assign(instance, instanceProps, nullFields); return instance; }; proto._type = factory; factory._schemaKeys = schemaKeys; factory._cumulativeSchema = Object.assign({}, schema); factory._isUsed = false; factory._typeHash = null; factory._isCustomType = true; factory._name = name; factory.prototype = proto; factory.description = name; factory._validator = function (value) { return (value instanceof factory); }; factory._ensureHash = function () { if (this._typeHash == null) { this._isUsed = true; this._typeHash = generateTypeHash(this); typeMap.set(this._typeHash, this); } }; /* FIXME: Is the below actually necessary? */ proto._selfValidator = factory._validator; factory._implementedTraits = new Set(); factory.implements = function (trait, implementation) { /* FIXME: Deregister old hash from global hash registry after mutating through trait implementation */ if (this._isUsed !== false) { throw new Error("Cannot modify a custom type that has already been instantiated"); } else { let {implementationDescriptors, slotSchemaKeys, implementationSchema} = trait.applyImplementation(implementation); let plainImplementationSchema = mapObj(implementationSchema, (key, value) => { return [key, value.rule]; }); // FIXME: Test this Object.keys(implementationDescriptors).forEach((key) => { if (proto.key != null) { throw new Error(`Trait implementation attempted to define a property '${key}' on the '${name}' type, but that property already exists`); } }); Object.defineProperties(proto, implementationDescriptors); this._implementedTraits.add(trait); this._schemaKeys = this._schemaKeys.concat(slotSchemaKeys); Object.assign(this._cumulativeSchema, plainImplementationSchema); /* TODO: Verify that this produces the expected performance benefit for trait-having typesw. */ for (let key of slotSchemaKeys) { protoProperties._data[key] = null; } return this; } }; factory.deserialize = function (data, deserializer) { hashAllTypes(); return deserialize(this, JSON.parse(data), typeMap, { deserializer: deserializer }); }; let rule = typeRules._createTypeRule(factory); registeredTypes.add(rule); registeredTypesDirty = true; return rule; } };