rocket.chat.mqtt
Version:
It's a MQTT Server, using redis to scale horizontally.
517 lines (483 loc) • 15.7 kB
JavaScript
/**
* @namespace
* @summary Namespace for EJSON functions
*/
EJSON = {};
EJSONTest = {};
// Custom type interface definition
/**
* @class CustomType
* @instanceName customType
* @memberOf EJSON
* @summary The interface that a class must satisfy to be able to become an
* EJSON custom type via EJSON.addType.
*/
/**
* @function typeName
* @memberOf EJSON.CustomType
* @summary Return the tag used to identify this type. This must match the tag used to register this type with [`EJSON.addType`](#ejson_add_type).
* @locus Anywhere
* @instance
*/
/**
* @function toJSONValue
* @memberOf EJSON.CustomType
* @summary Serialize this instance into a JSON-compatible value.
* @locus Anywhere
* @instance
*/
/**
* @function clone
* @memberOf EJSON.CustomType
* @summary Return a value `r` such that `this.equals(r)` is true, and modifications to `r` do not affect `this` and vice versa.
* @locus Anywhere
* @instance
*/
/**
* @function equals
* @memberOf EJSON.CustomType
* @summary Return `true` if `other` has a value equal to `this`; `false` otherwise.
* @locus Anywhere
* @param {Object} other Another object to compare this to.
* @instance
*/
var customTypes = {};
// Add a custom type, using a method of your choice to get to and
// from a basic JSON-able representation. The factory argument
// is a function of JSON-able --> your object
// The type you add must have:
// - A toJSONValue() method, so that Meteor can serialize it
// - a typeName() method, to show how to look it up in our type table.
// It is okay if these methods are monkey-patched on.
// EJSON.clone will use toJSONValue and the given factory to produce
// a clone, but you may specify a method clone() that will be
// used instead.
// Similarly, EJSON.equals will use toJSONValue to make comparisons,
// but you may provide a method equals() instead.
/**
* @summary Add a custom datatype to EJSON.
* @locus Anywhere
* @param {String} name A tag for your custom type; must be unique among custom data types defined in your project, and must match the result of your type's `typeName` method.
* @param {Function} factory A function that deserializes a JSON-compatible value into an instance of your type. This should match the serialization performed by your type's `toJSONValue` method.
*/
EJSON.addType = function (name, factory) {
if (_.has(customTypes, name))
throw new Error("Type " + name + " already present");
customTypes[name] = factory;
};
var isInfOrNan = function (obj) {
return _.isNaN(obj) || obj === Infinity || obj === -Infinity;
};
var builtinConverters = [
{ // Date
matchJSONValue: function (obj) {
return _.has(obj, '$date') && _.size(obj) === 1;
},
matchObject: function (obj) {
return obj instanceof Date;
},
toJSONValue: function (obj) {
return {$date: obj.getTime()};
},
fromJSONValue: function (obj) {
return new Date(obj.$date);
}
},
{ // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object'
// which we match.)
matchJSONValue: function (obj) {
return _.has(obj, '$InfNaN') && _.size(obj) === 1;
},
matchObject: isInfOrNan,
toJSONValue: function (obj) {
var sign;
if (_.isNaN(obj))
sign = 0;
else if (obj === Infinity)
sign = 1;
else
sign = -1;
return {$InfNaN: sign};
},
fromJSONValue: function (obj) {
return obj.$InfNaN/0;
}
},
{ // Binary
matchJSONValue: function (obj) {
return _.has(obj, '$binary') && _.size(obj) === 1;
},
matchObject: function (obj) {
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array
|| (obj && _.has(obj, '$Uint8ArrayPolyfill'));
},
toJSONValue: function (obj) {
return {$binary: Base64.encode(obj)};
},
fromJSONValue: function (obj) {
return Base64.decode(obj.$binary);
}
},
{ // Escaping one level
matchJSONValue: function (obj) {
return _.has(obj, '$escape') && _.size(obj) === 1;
},
matchObject: function (obj) {
if (_.isEmpty(obj) || _.size(obj) > 2) {
return false;
}
return _.any(builtinConverters, function (converter) {
return converter.matchJSONValue(obj);
});
},
toJSONValue: function (obj) {
var newObj = {};
_.each(obj, function (value, key) {
newObj[key] = EJSON.toJSONValue(value);
});
return {$escape: newObj};
},
fromJSONValue: function (obj) {
var newObj = {};
_.each(obj.$escape, function (value, key) {
newObj[key] = EJSON.fromJSONValue(value);
});
return newObj;
}
},
{ // Custom
matchJSONValue: function (obj) {
return _.has(obj, '$type') && _.has(obj, '$value') && _.size(obj) === 2;
},
matchObject: function (obj) {
return EJSON._isCustomType(obj);
},
toJSONValue: function (obj) {
var jsonValue = Meteor._noYieldsAllowed(function () {
return obj.toJSONValue();
});
return {$type: obj.typeName(), $value: jsonValue};
},
fromJSONValue: function (obj) {
var typeName = obj.$type;
if (!_.has(customTypes, typeName))
throw new Error("Custom EJSON type " + typeName + " is not defined");
var converter = customTypes[typeName];
return Meteor._noYieldsAllowed(function () {
return converter(obj.$value);
});
}
}
];
EJSON._isCustomType = function (obj) {
return obj &&
typeof obj.toJSONValue === 'function' &&
typeof obj.typeName === 'function' &&
_.has(customTypes, obj.typeName());
};
EJSON._getTypes = function () {
return customTypes;
};
EJSON._getConverters = function () {
return builtinConverters;
};
// for both arrays and objects, in-place modification.
var adjustTypesToJSONValue =
EJSON._adjustTypesToJSONValue = function (obj) {
// Is it an atom that we need to adjust?
if (obj === null)
return null;
var maybeChanged = toJSONValueHelper(obj);
if (maybeChanged !== undefined)
return maybeChanged;
// Other atoms are unchanged.
if (typeof obj !== 'object')
return obj;
// Iterate over array or object structure.
_.each(obj, function (value, key) {
if (typeof value !== 'object' && value !== undefined &&
!isInfOrNan(value))
return; // continue
var changed = toJSONValueHelper(value);
if (changed) {
obj[key] = changed;
return; // on to the next key
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
adjustTypesToJSONValue(value);
});
return obj;
};
// Either return the JSON-compatible version of the argument, or undefined (if
// the item isn't itself replaceable, but maybe some fields in it are)
var toJSONValueHelper = function (item) {
for (var i = 0; i < builtinConverters.length; i++) {
var converter = builtinConverters[i];
if (converter.matchObject(item)) {
return converter.toJSONValue(item);
}
}
return undefined;
};
/**
* @summary Serialize an EJSON-compatible value into its plain JSON representation.
* @locus Anywhere
* @param {EJSON} val A value to serialize to plain JSON.
*/
EJSON.toJSONValue = function (item) {
var changed = toJSONValueHelper(item);
if (changed !== undefined)
return changed;
if (typeof item === 'object') {
item = EJSON.clone(item);
adjustTypesToJSONValue(item);
}
return item;
};
// for both arrays and objects. Tries its best to just
// use the object you hand it, but may return something
// different if the object you hand it itself needs changing.
//
var adjustTypesFromJSONValue =
EJSON._adjustTypesFromJSONValue = function (obj) {
if (obj === null)
return null;
var maybeChanged = fromJSONValueHelper(obj);
if (maybeChanged !== obj)
return maybeChanged;
// Other atoms are unchanged.
if (typeof obj !== 'object')
return obj;
_.each(obj, function (value, key) {
if (typeof value === 'object') {
var changed = fromJSONValueHelper(value);
if (value !== changed) {
obj[key] = changed;
return;
}
// if we get here, value is an object but not adjustable
// at this level. recurse.
adjustTypesFromJSONValue(value);
}
});
return obj;
};
// Either return the argument changed to have the non-json
// rep of itself (the Object version) or the argument itself.
// DOES NOT RECURSE. For actually getting the fully-changed value, use
// EJSON.fromJSONValue
var fromJSONValueHelper = function (value) {
if (typeof value === 'object' && value !== null) {
if (_.size(value) <= 2
&& _.all(value, function (v, k) {
return typeof k === 'string' && k.substr(0, 1) === '$';
})) {
for (var i = 0; i < builtinConverters.length; i++) {
var converter = builtinConverters[i];
if (converter.matchJSONValue(value)) {
return converter.fromJSONValue(value);
}
}
}
}
return value;
};
/**
* @summary Deserialize an EJSON value from its plain JSON representation.
* @locus Anywhere
* @param {JSONCompatible} val A value to deserialize into EJSON.
*/
EJSON.fromJSONValue = function (item) {
var changed = fromJSONValueHelper(item);
if (changed === item && typeof item === 'object') {
item = EJSON.clone(item);
adjustTypesFromJSONValue(item);
return item;
} else {
return changed;
}
};
/**
* @summary Serialize a value to a string.
For EJSON values, the serialization fully represents the value. For non-EJSON values, serializes the same way as `JSON.stringify`.
* @locus Anywhere
* @param {EJSON} val A value to stringify.
* @param {Object} [options]
* @param {Boolean | Integer | String} options.indent Indents objects and arrays for easy readability. When `true`, indents by 2 spaces; when an integer, indents by that number of spaces; and when a string, uses the string as the indentation pattern.
* @param {Boolean} options.canonical When `true`, stringifies keys in an object in sorted order.
*/
EJSON.stringify = function (item, options) {
var json = EJSON.toJSONValue(item);
if (options && (options.canonical || options.indent)) {
return EJSON._canonicalStringify(json, options);
} else {
return JSON.stringify(json);
}
};
/**
* @summary Parse a string into an EJSON value. Throws an error if the string is not valid EJSON.
* @locus Anywhere
* @param {String} str A string to parse into an EJSON value.
*/
EJSON.parse = function (item) {
if (typeof item !== 'string')
throw new Error("EJSON.parse argument should be a string");
return EJSON.fromJSONValue(JSON.parse(item));
};
/**
* @summary Returns true if `x` is a buffer of binary data, as returned from [`EJSON.newBinary`](#ejson_new_binary).
* @param {Object} x The variable to check.
* @locus Anywhere
*/
EJSON.isBinary = function (obj) {
return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) ||
(obj && obj.$Uint8ArrayPolyfill));
};
/**
* @summary Return true if `a` and `b` are equal to each other. Return false otherwise. Uses the `equals` method on `a` if present, otherwise performs a deep comparison.
* @locus Anywhere
* @param {EJSON} a
* @param {EJSON} b
* @param {Object} [options]
* @param {Boolean} options.keyOrderSensitive Compare in key sensitive order, if supported by the JavaScript implementation. For example, `{a: 1, b: 2}` is equal to `{b: 2, a: 1}` only when `keyOrderSensitive` is `false`. The default is `false`.
*/
EJSON.equals = function (a, b, options) {
var i;
var keyOrderSensitive = !!(options && options.keyOrderSensitive);
if (a === b)
return true;
if (_.isNaN(a) && _.isNaN(b))
return true; // This differs from the IEEE spec for NaN equality, b/c we don't want
// anything ever with a NaN to be poisoned from becoming equal to anything.
if (!a || !b) // if either one is falsy, they'd have to be === to be equal
return false;
if (!(typeof a === 'object' && typeof b === 'object'))
return false;
if (a instanceof Date && b instanceof Date)
return a.valueOf() === b.valueOf();
if (EJSON.isBinary(a) && EJSON.isBinary(b)) {
if (a.length !== b.length)
return false;
for (i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
}
if (typeof (a.equals) === 'function')
return a.equals(b, options);
if (typeof (b.equals) === 'function')
return b.equals(a, options);
if (a instanceof Array) {
if (!(b instanceof Array))
return false;
if (a.length !== b.length)
return false;
for (i = 0; i < a.length; i++) {
if (!EJSON.equals(a[i], b[i], options))
return false;
}
return true;
}
// fallback for custom types that don't implement their own equals
switch (EJSON._isCustomType(a) + EJSON._isCustomType(b)) {
case 1: return false;
case 2: return EJSON.equals(EJSON.toJSONValue(a), EJSON.toJSONValue(b));
}
// fall back to structural equality of objects
var ret;
if (keyOrderSensitive) {
var bKeys = [];
_.each(b, function (val, x) {
bKeys.push(x);
});
i = 0;
ret = _.all(a, function (val, x) {
if (i >= bKeys.length) {
return false;
}
if (x !== bKeys[i]) {
return false;
}
if (!EJSON.equals(val, b[bKeys[i]], options)) {
return false;
}
i++;
return true;
});
return ret && i === bKeys.length;
} else {
i = 0;
ret = _.all(a, function (val, key) {
if (!_.has(b, key)) {
return false;
}
if (!EJSON.equals(val, b[key], options)) {
return false;
}
i++;
return true;
});
return ret && _.size(b) === i;
}
};
/**
* @summary Return a deep copy of `val`.
* @locus Anywhere
* @param {EJSON} val A value to copy.
*/
EJSON.clone = function (v) {
var ret;
if (typeof v !== "object")
return v;
if (v === null)
return null; // null has typeof "object"
if (v instanceof Date)
return new Date(v.getTime());
// RegExps are not really EJSON elements (eg we don't define a serialization
// for them), but they're immutable anyway, so we can support them in clone.
if (v instanceof RegExp)
return v;
if (EJSON.isBinary(v)) {
ret = EJSON.newBinary(v.length);
for (var i = 0; i < v.length; i++) {
ret[i] = v[i];
}
return ret;
}
// XXX: Use something better than underscore's isArray
if (_.isArray(v) || _.isArguments(v)) {
// For some reason, _.map doesn't work in this context on Opera (weird test
// failures).
ret = [];
for (i = 0; i < v.length; i++)
ret[i] = EJSON.clone(v[i]);
return ret;
}
// handle general user-defined typed Objects if they have a clone method
if (typeof v.clone === 'function') {
return v.clone();
}
// handle other custom types
if (EJSON._isCustomType(v)) {
return EJSON.fromJSONValue(EJSON.clone(EJSON.toJSONValue(v)), true);
}
// handle other objects
ret = {};
_.each(v, function (value, key) {
ret[key] = EJSON.clone(value);
});
return ret;
};
/**
* @summary Allocate a new buffer of binary data that EJSON can serialize.
* @locus Anywhere
* @param {Number} size The number of bytes of binary data to allocate.
*/
// EJSON.newBinary is the public documented API for this functionality,
// but the implementation is in the 'base64' package to avoid
// introducing a circular dependency. (If the implementation were here,
// then 'base64' would have to use EJSON.newBinary, and 'ejson' would
// also have to use 'base64'.)
EJSON.newBinary = Base64.newBinary;