@planjs/mobx-persist
Version:
enhance-mobx-persist
1,293 lines (1,171 loc) • 53.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('mobx')) :
typeof define === 'function' && define.amd ? define(['exports', 'mobx'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mobxPersist = {}, global.mobx));
})(this, (function (exports, mobx) { 'use strict';
/**
* Creates a model schema that (de)serializes from / to plain javascript objects.
* Its factory method is: `() => ({})`
*
* @example
* var todoSchema = createSimpleSchema({
* title: true,
* done: true,
* });
*
* var json = serialize(todoSchema, { title: 'Test', done: false });
* var todo = deserialize(todoSchema, json);
*
* @param {object} props property mapping,
* @returns {object} model schema
*/
function createSimpleSchema(props) {
return {
factory: function() {
return {}
},
props: props
}
}
var formatters = {
j: function json(v) {
try {
return JSON.stringify(v)
} catch (error) {
return "[UnexpectedJSONParseError]: " + error.message
}
}
};
function invariant(condition, message) {
if (!condition) {
var variables = Array.prototype.slice.call(arguments, 2);
var variablesToLog = [];
var index = 0;
var formattedMessage = message.replace(/%([a-zA-Z%])/g, function messageFormatter(match, format) {
if (match === "%%") return match
var formatter = formatters[format];
if (typeof formatter === "function") {
var variable = variables[index++];
variablesToLog.push(variable);
return formatter(variable)
}
return match
});
if (console && variablesToLog.length > 0) {
// eslint-disable-next-line no-console
console.log.apply(console, variablesToLog);
}
throw new Error("[serializr] " + (formattedMessage || "Illegal State"))
}
}
function GUARDED_NOOP(err) {
if (err) // unguarded error...
throw new Error(err)
}
function once(fn) {
var fired = false;
return function () {
if (!fired) {
fired = true;
return fn.apply(null, arguments)
}
invariant(false, "callback was invoked twice");
}
}
function parallel(ar, processor, cb) {
// TODO: limit parallelization?
if (ar.length === 0)
return void cb(null, [])
var left = ar.filter(function(){ return true }).length; // only count items processed by forEach
var resultArray = [];
var failed = false;
var processorCb = function (idx, err, result) {
if (err) {
if (!failed) {
failed = true;
cb(err);
}
} else {
resultArray[idx] = result;
if (--left === 0)
cb(null, resultArray);
}
};
ar.forEach(function (value, idx) {
processor(value, processorCb.bind(null, idx), idx);
});
}
function isPrimitive(value) {
if (value === null)
return true
return typeof value !== "object" && typeof value !== "function"
}
function isModelSchema(thing) {
return thing && thing.factory && thing.props
}
function isPropSchema(thing) {
return thing && thing.serializer && thing.deserializer
}
function isAliasedPropSchema(propSchema) {
return typeof propSchema === "object" && !!propSchema.jsonname
}
function isAssignableTo(actualType, expectedType) {
while (actualType) {
if (actualType === expectedType)
return true
actualType = actualType.extends;
}
return false
}
function isMapLike(thing) {
return thing && typeof thing.keys === "function" && typeof thing.clear === "function"
}
function processAdditionalPropArgs(propSchema, additionalArgs) {
if (additionalArgs) {
invariant(isPropSchema(propSchema), "expected a propSchema");
var argNames = ["beforeDeserialize", "afterDeserialize"];
argNames.forEach(function(argName) {
if (typeof additionalArgs[argName] === "function") {
propSchema[argName] = additionalArgs[argName];
}
});
}
return propSchema
}
/**
* Returns the standard model schema associated with a class / constructor function
*
* @param {object} thing
* @returns {ModelSchema} model schema
*/
function getDefaultModelSchema(thing) {
if (!thing)
return null
if (isModelSchema(thing))
return thing
if (isModelSchema(thing.serializeInfo))
return thing.serializeInfo
if (thing.constructor && thing.constructor.serializeInfo)
return thing.constructor.serializeInfo
}
/**
* Sets the default model schema for class / constructor function.
* Everywhere where a model schema is required as argument, this class / constructor function
* can be passed in as well (for example when using `object` or `ref`.
*
* When passing an instance of this class to `serialize`, it is not required to pass the model schema
* as first argument anymore, because the default schema will be inferred from the instance type.
*
* @param {constructor|class} clazz class or constructor function
* @param {ModelSchema} modelSchema - a model schema
* @returns {ModelSchema} model schema
*/
function setDefaultModelSchema(clazz, modelSchema) {
invariant(isModelSchema(modelSchema));
return clazz.serializeInfo = modelSchema
}
/**
* Creates a model schema that (de)serializes an object created by a constructor function (class).
* The created model schema is associated by the targeted type as default model schema, see setDefaultModelSchema.
* Its factory method is `() => new clazz()` (unless overriden, see third arg).
*
* @example
* function Todo(title, done) {
* this.title = title;
* this.done = done;
* }
*
* createModelSchema(Todo, {
* title: true,
* done: true,
* });
*
* var json = serialize(new Todo('Test', false));
* var todo = deserialize(Todo, json);
*
* @param {constructor|class} clazz class or constructor function
* @param {object} props property mapping
* @param {function} factory optional custom factory. Receives context as first arg
* @returns {object} model schema
*/
function createModelSchema(clazz, props, factory) {
invariant(clazz !== Object, "one cannot simply put define a model schema for Object");
invariant(typeof clazz === "function", "expected constructor function");
var model = {
targetClass: clazz,
factory: factory || function() {
return new clazz()
},
props: props
};
// find super model
if (clazz.prototype.constructor !== Object) {
var s = getDefaultModelSchema(clazz.prototype.constructor);
if (s && s.targetClass !== clazz)
model.extends = s;
}
setDefaultModelSchema(clazz, model);
return model
}
/**
* Indicates that this field contains a primitive value (or Date) which should be serialized literally to json.
*
* @example
* createModelSchema(Todo, {
* title: primitive(),
* });
*
* console.dir(serialize(new Todo('test')));
* // outputs: { title : "test" }
*
* @param {AdditionalPropArgs} additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers
* @returns {ModelSchema}
*/
function primitive(additionalArgs) {
var result = {
serializer: function (value) {
invariant(isPrimitive(value), "this value is not primitive: " + value);
return value
},
deserializer: function (jsonValue, done) {
if (!isPrimitive(jsonValue))
return void done("[serializr] this value is not primitive: " + jsonValue)
return void done(null, jsonValue)
}
};
result = processAdditionalPropArgs(result, additionalArgs);
return result
}
/**
* In the event that a property needs to be deserialized, but not serialized, you can use the SKIP symbol to omit the property. This has to be used with the custom serializer.
*
* @example
* var schema = _.createSimpleSchema({
* a: _.custom(
* function(v) {
* return _.SKIP
* },
* function(v) {
* return v;
* }
* ),
* });
* t.deepEqual(_.serialize(s, { a: 4 }), { });
* t.deepEqual(_.deserialize(s, { a: 4 }), { a: 4 });
*/
var SKIP = typeof Symbol !== "undefined" ? Symbol("SKIP") : { SKIP: true };
var _defaultPrimitiveProp = primitive();
// Ugly way to get the parameter names since they aren't easily retrievable via reflection
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
function getParamNames(func) {
var fnStr = func.toString().replace(STRIP_COMMENTS, "");
var result = fnStr.slice(fnStr.indexOf("(")+1, fnStr.indexOf(")")).match(ARGUMENT_NAMES);
if(result === null)
result = [];
return result
}
function serializableDecorator(propSchema, target, propName, descriptor) {
invariant(arguments.length >= 2, "too few arguments. Please use @serializable as property decorator");
// Fix for @serializable used in class constructor params (typescript)
var factory;
if (propName === undefined && typeof target === "function"
&& target.prototype
&& descriptor !== undefined && typeof descriptor === "number") {
invariant(isPropSchema(propSchema), "Constructor params must use alias(name)");
invariant(propSchema.jsonname, "Constructor params must use alias(name)");
var paramNames = getParamNames(target);
if (paramNames.length >= descriptor) {
propName = paramNames[descriptor];
propSchema.paramNumber = descriptor;
descriptor = undefined;
target = target.prototype;
// Create a factory so the constructor is called properly
factory = function(context) {
var params = [];
for (var i = 0; i < target.constructor.length; i++) {
Object.keys(context.modelSchema.props).forEach(function (key) {
var prop = context.modelSchema.props[key];
if (prop.paramNumber === i) {
params[i] = context.json[prop.jsonname];
}
});
}
return new (Function.prototype.bind.apply(target.constructor, [null].concat(params)))
};
}
}
invariant(typeof propName === "string", "incorrect usage of @serializable decorator");
var info = getDefaultModelSchema(target);
if (!info || !target.constructor.hasOwnProperty("serializeInfo"))
info = createModelSchema(target.constructor, {}, factory);
if (info && info.targetClass !== target.constructor)
// fixes typescript issue that tends to copy fields from super constructor to sub constructor in extends
info = createModelSchema(target.constructor, {}, factory);
info.props[propName] = propSchema;
// MWE: why won't babel work without?
if (descriptor && !descriptor.get && !descriptor.set)
descriptor.writable = true;
return descriptor
}
/**
* Decorator that defines a new property mapping on the default model schema for the class
* it is used in.
*
* When using typescript, the decorator can also be used on fields declared as constructor arguments (using the `private` / `protected` / `public` keywords).
* The default factory will then invoke the constructor with the correct arguments as well.
*
* @example
* class Todo {
* @serializable(primitive())
* title; // shorthand for primitves
*
* @serializable done;
*
* constructor(title, done) {
* this.title = title;
* this.done = done;
* }
* }
*
* var json = serialize(new Todo('Test', false));
* var todo = deserialize(Todo, json);
*
* @param arg1
* @param arg2
* @param arg3
* @returns {PropertyDescriptor}
*/
function serializable(arg1, arg2, arg3) {
if (arguments.length === 1) {
// decorated with propSchema
var propSchema = arg1 === true ? _defaultPrimitiveProp : arg1;
invariant(isPropSchema(propSchema), "@serializable expects prop schema");
return serializableDecorator.bind(null, propSchema)
} else {
// decorated without arguments, treat as primitive
return serializableDecorator(primitive(), arg1, arg2, arg3)
}
}
/**
* Serializes an object (graph) into json using the provided model schema.
* The model schema can be omitted if the object type has a default model schema associated with it.
* If a list of objects is provided, they should have an uniform type.
*
* @param arg1 class or modelschema to use. Optional
* @param arg2 object(s) to serialize
* @returns {object} serialized representation of the object
*/
function serialize(arg1, arg2) {
invariant(arguments.length === 1 || arguments.length === 2, "serialize expects one or 2 arguments");
var thing = arguments.length === 1 ? arg1 : arg2;
var schema = arguments.length === 1 ? null : arg1;
if (Array.isArray(thing)) {
if (thing.length === 0)
return [] // don't bother finding a schema
else if (!schema)
schema = getDefaultModelSchema(thing[0]);
else if (typeof schema !== "object")
schema = getDefaultModelSchema(schema);
} else if (!schema) {
schema = getDefaultModelSchema(thing);
} else if (typeof schema !== "object") {
schema = getDefaultModelSchema(schema);
}
invariant(!!schema, "Failed to find default schema for " + arg1);
if (Array.isArray(thing))
return thing.map(function (item) {
return serializeWithSchema(schema, item)
})
return serializeWithSchema(schema, thing)
}
function checkStarSchemaInvariant(propDef) {
invariant(propDef === true || propDef.pattern, "prop schema '*' can only be used with 'true' or a prop def with a 'pattern': " + JSON.stringify(propDef));
}
function serializeWithSchema(schema, obj) {
invariant(schema && typeof schema === "object" && schema.props, "Expected schema");
invariant(obj && typeof obj === "object", "Expected object");
var res;
if (schema.extends)
res = serializeWithSchema(schema.extends, obj);
else {
// TODO: make invariant?: invariant(!obj.constructor.prototype.constructor.serializeInfo, "object has a serializable supertype, but modelschema did not provide extends clause")
res = {};
}
Object.keys(schema.props).forEach(function (key) {
var propDef = schema.props[key];
if (key === "*") {
serializeStarProps(schema, propDef, obj, res);
return
}
if (propDef === true)
propDef = _defaultPrimitiveProp;
if (propDef === false)
return
var jsonValue = propDef.serializer(obj[key], key, obj);
if (jsonValue === SKIP){
return
}
res[propDef.jsonname || key] = jsonValue;
});
return res
}
function serializeStarProps(schema, propDef, obj, target) {
checkStarSchemaInvariant(propDef);
for (var key in obj) if (obj.hasOwnProperty(key)) if (!(key in schema.props)) {
if ((propDef === true) || (propDef.pattern && propDef.pattern.test(key))) {
var value = obj[key];
if (propDef === true) {
if (isPrimitive(value)) {
target[key] = value;
}
} else if (propDef.props) {
var jsonValue = serialize(propDef, value);
if (jsonValue === SKIP){
return
}
// todo: propDef.jsonname could be a transform function on key
target[key] = jsonValue;
} else {
var jsonValue = propDef.serializer(value, key, obj);
if (jsonValue === SKIP){
return
}
// todo: propDef.jsonname could be a transform function on key
target[key] = jsonValue;
}
}
}
}
var rootContextCache = new WeakMap();
function Context(parentContext, modelSchema, json, onReadyCb, customArgs) {
this.parentContext = parentContext;
this.isRoot = !parentContext;
this.pendingCallbacks = 0;
this.pendingRefsCount = 0;
this.onReadyCb = onReadyCb || GUARDED_NOOP;
this.json = json;
this.target = null; // always set this property using setTarget
this.hasError = false;
this.modelSchema = modelSchema;
if (this.isRoot) {
this.rootContext = this;
this.args = customArgs;
this.pendingRefs = {}; // uuid: [{ modelSchema, uuid, cb }]
this.resolvedRefs = {}; // uuid: [{ modelSchema, value }]
} else {
this.rootContext = parentContext.rootContext;
this.args = parentContext.args;
}
}
Context.prototype.createCallback = function (fn) {
this.pendingCallbacks++;
// once: defend against user-land calling 'done' twice
return once(function (err, value) {
if (err) {
if (!this.hasError) {
this.hasError = true;
this.onReadyCb(err);
rootContextCache.delete(this);
}
} else if (!this.hasError) {
fn(value);
if (--this.pendingCallbacks === this.pendingRefsCount) {
if (this.pendingRefsCount > 0) {
// all pending callbacks are pending reference resolvers. not good.
this.onReadyCb(new Error(
"Unresolvable references in json: \"" +
Object.keys(this.pendingRefs).filter(function (uuid) {
return this.pendingRefs[uuid].length > 0
}, this).join("\", \"") +
"\""
));
rootContextCache.delete(this);
} else {
this.onReadyCb(null, this.target);
rootContextCache.delete(this);
}
}
}
}.bind(this))
};
// given an object with uuid, modelSchema, callback, awaits until the given uuid is available
// resolve immediately if possible
Context.prototype.await = function (modelSchema, uuid, callback) {
invariant(this.isRoot);
if (uuid in this.resolvedRefs) {
var match = this.resolvedRefs[uuid].filter(function (resolved) {
return isAssignableTo(resolved.modelSchema, modelSchema)
})[0];
if (match)
return void callback(null, match.value)
}
this.pendingRefsCount++;
if (!this.pendingRefs[uuid])
this.pendingRefs[uuid] = [];
this.pendingRefs[uuid].push({
modelSchema: modelSchema,
uuid: uuid,
callback: callback
});
};
// given a model schema, uuid and value, resolve all references that where looking for this object
Context.prototype.resolve = function (modelSchema, uuid, value) {
invariant(this.isRoot);
if (!this.resolvedRefs[uuid])
this.resolvedRefs[uuid] = [];
this.resolvedRefs[uuid].push({
modelSchema: modelSchema, value: value
});
if (uuid in this.pendingRefs) {
for (var i = this.pendingRefs[uuid].length - 1; i >= 0; i--) {
var opts = this.pendingRefs[uuid][i];
if (isAssignableTo(modelSchema, opts.modelSchema)) {
this.pendingRefs[uuid].splice(i, 1);
this.pendingRefsCount--;
opts.callback(null, value);
}
}
}
};
// set target and update root context cache
Context.prototype.setTarget = function (target) {
if (this.isRoot && this.target) {
rootContextCache.delete(this.target);
}
this.target = target;
rootContextCache.set(this.target, this);
};
// call all remaining reference lookup callbacks indicating an error during ref resolution
Context.prototype.cancelAwaits = function () {
invariant(this.isRoot);
var self = this;
Object.keys(this.pendingRefs).forEach(function (uuid) {
self.pendingRefs[uuid].forEach(function (refOpts) {
self.pendingRefsCount--;
refOpts.callback(new Error("Reference resolution canceled for " + uuid));
});
});
this.pendingRefs = {};
this.pendingRefsCount = 0;
};
/*
* Deserialization
*/
function schemaHasAlias(schema, name) {
for (var key in schema.props)
if (typeof schema.props[key] === "object" && schema.props[key].jsonname === name)
return true
return false
}
function deserializeStarProps(context, schema, propDef, obj, json) {
checkStarSchemaInvariant(propDef);
for (var key in json) if (!(key in schema.props) && !schemaHasAlias(schema, key)) {
var jsonValue = json[key];
if (propDef === true) {
// when deserializing we don't want to silently ignore 'unparseable data' to avoid
// confusing bugs
invariant(isPrimitive(jsonValue),
"encountered non primitive value while deserializing '*' properties in property '" +
key + "': " + jsonValue);
obj[key] = jsonValue;
} else if (propDef.pattern.test(key)) {
if (propDef.factory) {
var resultValue = deserializeObjectWithSchema(context, propDef, jsonValue, context.callback || GUARDED_NOOP, {});
// deserializeObjectWithSchema returns undefined on error
if (resultValue !== undefined) {
obj[key] = resultValue;
}
} else {
function setValue(resultValue) {
if (resultValue !== SKIP) {
obj[key] = resultValue;
}
}
propDef.deserializer(jsonValue,
// for individual props, use root context based callbacks
// this allows props to complete after completing the object itself
// enabling reference resolving and such
context.rootContext.createCallback(setValue),
context);
}
}
}
}
function deserializeObjectWithSchema(parentContext, modelSchema, json, callback, customArgs) {
if (json === null || json === undefined || typeof json !== "object")
return void callback(null, null)
var context = new Context(parentContext, modelSchema, json, callback, customArgs);
var target = modelSchema.factory(context);
// todo async invariant
invariant(!!target, "No object returned from factory");
// TODO: make invariant? invariant(schema.extends ||
// !target.constructor.prototype.constructor.serializeInfo, "object has a serializable
// supertype, but modelschema did not provide extends clause")
context.setTarget(target);
var lock = context.createCallback(GUARDED_NOOP);
deserializePropsWithSchema(context, modelSchema, json, target);
lock();
return target
}
function deserializePropsWithSchema(context, modelSchema, json, target) {
if (modelSchema.extends)
deserializePropsWithSchema(context, modelSchema.extends, json, target);
function deserializeProp(propDef, jsonValue, propName) {
function setValue(value) {
if (value !== SKIP) {
target[propName] = value;
}
}
function preProcess(resultCallback) {
return function (err, newValue) {
function finalCallback(errPreliminary, finalOrRetryValue) {
if (errPreliminary && finalOrRetryValue !== undefined &&
typeof propDef.afterDeserialize === "function") {
propDef.deserializer(
finalOrRetryValue,
preProcess(resultCallback),
context,
target[propName]
);
} else {
resultCallback(errPreliminary, finalOrRetryValue);
}
}
onAfterDeserialize(finalCallback, err, newValue, jsonValue, json,
propName, context, propDef);
}
}
propDef.deserializer(
jsonValue,
// for individual props, use root context based callbacks
// this allows props to complete after completing the object itself
// enabling reference resolving and such
preProcess(context.rootContext.createCallback(setValue)),
context,
target[propName] // initial value
);
}
Object.keys(modelSchema.props).forEach(function (propName) {
var propDef = modelSchema.props[propName];
function callbackDeserialize(err, jsonValue) {
if (!err && jsonValue !== undefined) {
deserializeProp(propDef, jsonValue, propName);
}
}
if (propName === "*") {
deserializeStarProps(context, modelSchema, propDef, target, json);
return
}
if (propDef === true)
propDef = _defaultPrimitiveProp;
if (propDef === false)
return
var jsonAttr = propDef.jsonname || propName;
var jsonValue = json[jsonAttr];
onBeforeDeserialize(callbackDeserialize, jsonValue, json, jsonAttr, context, propDef);
});
}
function onBeforeDeserialize(
callback, jsonValue, jsonParentValue, propNameOrIndex, context, propDef) {
if (propDef && typeof propDef.beforeDeserialize === "function") {
propDef.beforeDeserialize(callback, jsonValue, jsonParentValue, propNameOrIndex, context,
propDef);
} else {
callback(null, jsonValue);
}
}
function onAfterDeserialize(
callback, err, newValue, jsonValue, jsonParentValue, propNameOrIndex, context, propDef) {
if (propDef && typeof propDef.afterDeserialize === "function") {
propDef.afterDeserialize(callback, err, newValue, jsonValue, jsonParentValue,
propNameOrIndex, context, propDef);
} else {
callback(err, newValue);
}
}
/**
* `object` indicates that this property contains an object that needs to be (de)serialized
* using its own model schema.
*
* N.B. mind issues with circular dependencies when importing model schema's from other files! The module resolve algorithm might expose classes before `createModelSchema` is executed for the target class.
*
* @example
* class SubTask {}
* class Todo {}
*
* createModelSchema(SubTask, {
* title: true,
* });
* createModelSchema(Todo, {
* title: true,
* subTask: object(SubTask),
* });
*
* const todo = deserialize(Todo, {
* title: 'Task',
* subTask: {
* title: 'Sub task',
* },
* });
*
* @param {ModelSchema} modelSchema to be used to (de)serialize the object
* @param {AdditionalPropArgs} additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers
* @returns {PropSchema}
*/
function object(modelSchema, additionalArgs) {
invariant(typeof modelSchema === "object" || typeof modelSchema === "function", "No modelschema provided. If you are importing it from another file be aware of circular dependencies.");
var result = {
serializer: function (item) {
modelSchema = getDefaultModelSchema(modelSchema);
invariant(isModelSchema(modelSchema), "expected modelSchema, got " + modelSchema);
if (item === null || item === undefined)
return item
return serialize(modelSchema, item)
},
deserializer: function (childJson, done, context) {
modelSchema = getDefaultModelSchema(modelSchema);
invariant(isModelSchema(modelSchema), "expected modelSchema, got " + modelSchema);
if (childJson === null || childJson === undefined)
return void done(null, childJson)
return void deserializeObjectWithSchema(context, modelSchema, childJson, done, additionalArgs)
}
};
result = processAdditionalPropArgs(result, additionalArgs);
return result
}
/*
* Update
*/
/**
* Similar to deserialize, but updates an existing object instance.
* Properties will always updated entirely, but properties not present in the json will be kept as is.
* Further this method behaves similar to deserialize.
*
* @param {object} modelSchema, optional if it can be inferred from the instance type
* @param {object} target target instance to update
* @param {object} json the json to deserialize
* @param {function} callback the callback to invoke once deserialization has completed.
* @param {*} customArgs custom arguments that are available as `context.args` during the deserialization process. This can be used as dependency injection mechanism to pass in, for example, stores.
* @returns {object|array} deserialized object, possibly incomplete.
*/
function update(modelSchema, target, json, callback, customArgs) {
var inferModelSchema =
arguments.length === 2 // only target and json
|| typeof arguments[2] === "function"; // callback as third arg
if (inferModelSchema) {
target = arguments[0];
modelSchema = getDefaultModelSchema(target);
json = arguments[1];
callback = arguments[2];
customArgs = arguments[3];
} else {
modelSchema = getDefaultModelSchema(modelSchema);
}
invariant(isModelSchema(modelSchema), "update failed to determine schema");
invariant(typeof target === "object" && target && !Array.isArray(target), "update needs an object");
var context = new Context(null, modelSchema, json, callback, customArgs);
context.setTarget(target);
var lock = context.createCallback(GUARDED_NOOP);
var result = deserializePropsWithSchema(context, modelSchema, json, target);
lock();
return result
}
/**
* Can be used to create simple custom propSchema. Multiple things can be done inside of a custom propSchema, like deserializing and serializing other (polymorphic) objects, skipping the serialization of something or checking the context of the obj being (de)serialized.
* The `custom` function takes two parameters, the `serializer` function and the `deserializer` function.
* The `serializer` function has the signature:
* `(value, key, obj) => void`
* When serializing the object `{a: 1}` the `serializer` function will be called with `serializer(1, 'a', {a: 1})`.
* The `deserializer` function has the following signature for synchronous processing
* `(value, context, oldValue) => void`
* For asynchronous processing the function expects the following signature
* `(value, context, oldValue, callback) => void`
* When deserializing the object `{b: 2}` the `deserializer` function will be called with `deserializer(2, contextObj)` ([contextObj reference](https://github.com/mobxjs/serializr#deserialization-context)).
*
* @example
* var schemaDefault = _.createSimpleSchema({
* a: _.custom(
* function(v) {
* return v + 2;
* },
* function(v) {
* return v - 2;
* }
* ),
* });
* t.deepEqual(_.serialize(schemaDefault, { a: 4 }), { a: 6 });
* t.deepEqual(_.deserialize(schemaDefault, { a: 6 }), { a: 4 });
*
* var schemaWithAsyncProps = _.createSimpleSchema({
* a: _.customAsync(
* function(v) {
* return v + 2;
* },
* function(v, context, oldValue, callback) {
* somePromise(v, context, oldValue).then((result) => {
* callback(null, result - 2)
* }.catch((err) => {
* callback(err)
* }
* }
* ),
* });
* t.deepEqual(_.serialize(schemaWithAsyncProps, { a: 4 }), { a: 6 });
* _.deserialize(schemaWithAsyncProps, { a: 6 }, (err, res) => {
* t.deepEqual(res.a, 4)
* };
*
* @param {function} serializer function that takes a model value and turns it into a json value
* @param {function} deserializer function that takes a json value and turns it into a model value. It also takes context argument, which can allow you to deserialize based on the context of other parameters.
* @param {AdditionalPropArgs} additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers
* @returns {PropSchema}
*/
function custom(serializer, deserializer, additionalArgs) {
invariant(typeof serializer === "function", "first argument should be function");
invariant((typeof deserializer === "function"), "second argument should be a function or promise");
var result = {
serializer: serializer,
deserializer: function (jsonValue, done, context, oldValue) {
if (deserializer.length === 4) {
deserializer(jsonValue, context, oldValue, done, additionalArgs);
} else {
done(null, deserializer(jsonValue, context, oldValue, null, additionalArgs));
}
}
};
result = processAdditionalPropArgs(result, additionalArgs);
return result
}
/**
* List indicates that this property contains a list of things.
* Accepts a sub model schema to serialize the contents
*
* @example
* class SubTask {}
* class Task {}
* class Todo {}
*
* createModelSchema(SubTask, {
* title: true,
* });
* createModelSchema(Todo, {
* title: true,
* subTask: list(object(SubTask)),
* });
*
* const todo = deserialize(Todo, {
* title: 'Task',
* subTask: [
* {
* title: 'Sub task 1',
* },
* ],
* });
*
* @param {PropSchema} propSchema to be used to (de)serialize the contents of the array
* @param {AdditionalPropArgs} additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers
* @returns {PropSchema}
*/
function list(propSchema, additionalArgs) {
propSchema = propSchema || _defaultPrimitiveProp;
invariant(isPropSchema(propSchema), "expected prop schema as first argument");
invariant(!isAliasedPropSchema(propSchema),
"provided prop is aliased, please put aliases first");
var result = {
serializer: function (ar) {
if (ar === undefined) {
return SKIP
}
invariant(ar && "length" in ar && "map" in ar, "expected array (like) object");
return ar.map(propSchema.serializer)
},
deserializer: function (jsonArray, done, context) {
if (!Array.isArray(jsonArray))
return void done("[serializr] expected JSON array")
function processItem(jsonValue, onItemDone, itemIndex) {
function callbackBefore(err, value) {
if (!err) {
propSchema.deserializer(value, deserializeDone, context);
} else {
onItemDone(err);
}
}
function deserializeDone(err, value) {
if (typeof propSchema.afterDeserialize === "function") {
onAfterDeserialize(callbackAfter, err, value, jsonValue, itemIndex, context,
propSchema);
} else {
onItemDone(err, value);
}
}
function callbackAfter(errPreliminary, finalOrRetryValue) {
if (errPreliminary && finalOrRetryValue !== undefined &&
typeof propSchema.afterDeserialize === "function") {
propSchema.deserializer(
finalOrRetryValue,
deserializeDone,
context
);
} else {
onItemDone(errPreliminary, finalOrRetryValue);
}
}
onBeforeDeserialize(callbackBefore, jsonValue, jsonArray, itemIndex, context,
propSchema);
}
parallel(
jsonArray,
processItem,
done
);
}
};
result = processAdditionalPropArgs(result, additionalArgs);
return result
}
/**
* Similar to list, but map represents a string keyed dynamic collection.
* This can be both plain objects (default) or ES6 Map like structures.
* This will be inferred from the initial value of the targetted attribute.
*
* @param {*} propSchema
* @param {AdditionalPropArgs} additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers
* @returns {PropSchema}
*/
function map(propSchema, additionalArgs) {
propSchema = propSchema || _defaultPrimitiveProp;
invariant(isPropSchema(propSchema), "expected prop schema as first argument");
invariant(!isAliasedPropSchema(propSchema), "provided prop is aliased, please put aliases first");
var res = {
serializer: function (m) {
invariant(m && typeof m === "object", "expected object or Map");
var isMap = isMapLike(m);
var result = {};
if (isMap)
m.forEach(function (value, key) {
result[key] = propSchema.serializer(value);
});
else for (var key in m)
result[key] = propSchema.serializer(m[key]);
return result
},
deserializer: function (jsonObject, done, context, oldValue) {
if (!jsonObject || typeof jsonObject !== "object")
return void done("[serializr] expected JSON object")
var keys = Object.keys(jsonObject);
list(propSchema, additionalArgs).deserializer(
keys.map(function (key) {
return jsonObject[key]
}),
function (err, values) {
if (err)
return void done(err)
var isMap = isMapLike(oldValue);
var newValue;
if (isMap) {
// if the oldValue is a map, we recycle it
// there are many variations and this way we don't have to
// know about the original constructor
oldValue.clear();
newValue = oldValue;
} else
newValue = {};
for (var i = 0, l = keys.length; i < l; i++)
if (isMap)
newValue.set(keys[i], values[i]);
else
newValue[keys[i]] = values[i];
done(null, newValue);
},
context
);
}
};
res = processAdditionalPropArgs(res, additionalArgs);
return res
}
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function (obj) {
return typeof obj;
};
} else {
_typeof = function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof(obj);
}
function _walk(v) {
if (_typeof(v) === 'object' && v) Object.keys(v).map(function (k) {
return _walk(v[k]);
});
return v;
}
function _default() {
return custom(_walk, function (v) {
return v;
});
}
var types = {
object: function object$1(s) {
return s ? object(s) : _default();
},
list: function list$1(s) {
return list(this.object(s));
},
map: function map$1(s) {
return map(this.object(s));
}
};
function persistObject(target, schema) {
var model = createModel(schema);
setDefaultModelSchema(target, model);
return target;
}
function createModel(params) {
var schema = {};
Object.keys(params).forEach(function (key) {
if (_typeof(params[key]) === 'object') {
if (params[key].type in types) {
if (_typeof(params[key].schema) === 'object') {
schema[key] = types[params[key].type](createModel(params[key].schema));
} else {
schema[key] = types[params[key].type](params[key].schema);
}
}
} else if (params[key] === true) {
schema[key] = true;
}
});
return createSimpleSchema(schema);
}
function persist() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var a = args[0],
b = args[1];
if (a in types) {
return serializable(types[a](b));
}
if (args.length === 1) {
return function (target) {
return persistObject(target, a);
};
} // eslint-disable-next-line prefer-spread
return serializable.apply(null, args);
}
/**
* Basic Storage
*/
var IStorage =
/** @class */
function () {
function IStorage() {}
IStorage.prototype.getItem = function (key) {
throw new Error('IStorage has to implement `getItem` method');
};
IStorage.prototype.removeItem = function (key) {
throw new Error('IStorage has to implement `removeItem` method');
};
IStorage.prototype.setItem = function (key, value) {
throw new Error('IStorage has to implement `setItem` method');
};
IStorage.prototype.clear = function () {
throw new Error('IStorage has to implement `clear` method');
};
IStorage.prototype.onChange = function (key, newValue, oldValue) {};
return IStorage;
}();
function mergeObservables(target, source, keys) {
var t = target;
var s = source;
if (_typeof(t) === 'object' && _typeof(s) === 'object') {
for (var key in t) {
if (t[key] && _typeof(t[key]) === 'object' && _typeof(s[key]) === 'object') {
if (mobx.isObservableMap(t[key])) t[key].merge(s[key]);else if (mobx.isObservableArray(t[key])) t[key].replace(s[key]);else if (mobx.isObservableObject(t[key])) t[key] = mergeObservables(t[key], s[key]);
} else if (s[key] !== undefined) {
t[key] = s[key];
}
}
}
return t;
}
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var LocalStorage =
/** @class */
function (_super) {
__extends(LocalStorage, _super);
function LocalStorage() {
var _this = _super.call(this) || this;
try {
window.addEventListener('storage', function (ev) {
var _a;
(_a = _this.onChange) === null || _a === void 0 ? void 0 : _a.call(_this, ev.key, ev.newValue, ev.oldValue);
});
} catch (e) {
console.error('[mobx-persist] LocalStorage error', e);
}
return _this;
}
LocalStorage.prototype.getItem = function (key) {
return new Promise(function (resolve, reject) {
try {
var value = window.localStorage.getItem(key);
resolve(value);
} catch (err) {
reject(err);
}
});
};
LocalStorage.prototype.removeItem = function (key) {
return new Promise(function (resolve, reject) {
try {
window.localStorage.removeItem(key);
resolve(null);
} catch (err) {
reject(err);
}
});
};
LocalStorage.prototype.setItem = function (key, value) {
return new Promise(function (resolve, reject) {
try {
window.localStorage.setItem(key, value);
resolve(null);
} catch (err) {
reject(err);
}
});
};
LocalStorage.prototype.clear = function () {
return new Promise(function (resolv