Skip to content

Commit

Permalink
[Feature] lifecycle handlers for advanced deserialization and error h…
Browse files Browse the repository at this point in the history
…andling (#90)

* added propSchema customAsync. It allows asynchronous custom property deserialization. Especially, it allows to wait for resolution of references inside custom properties.

* added propSchema customAsync. It allows asynchronous custom property deserialization. Especially, it allows to wait for resolution of references inside custom properties.

* added test coverage for custom and customAsync propSchema

* added optional callback to custom propScheme

* removed customAsync, custom deserializer with callback should be used

* fix

* final polish, removed build error

* set version to 1.2.0

* added support for lifecycle methods in deserialization

* added cancelDeserialize and atomic livecycle methods for deserialization

* don't test mapAsArray for lifeCycle handlers

* fix Object.entries not available in node 6

* fix mapAsArray to always create map objects for uninitialized values

* fix package json (remove editor specific param)

* fix mapAsArray to always push elements to serialized array

* updated documentation

* Update tsconfig.json

* Update README.md
  • Loading branch information
1R053 authored and alexggordon committed Jan 23, 2019
1 parent c85a637 commit e46f798
Show file tree
Hide file tree
Showing 23 changed files with 855 additions and 196 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.4.0
* Introduced beforeDeserialize and afterDeserialize lifecycle methods to support e.g. better error handling during deserialization by @evoye
* Introduced cancelDeserialize to stop async requests that get stuck, e.g. in case of unresolved waiting references by @evoye
* Added capability to deserialize arrays with empty entries by @evoye
* Fixed mapAsArray to always push elements to the serialized array by @evoye

# 1.3.0
* Introduced async ability into `custom` (de)serializer, to support asynchronous custom deserialization by @evoye
* Fixed missed typescript export of `raw` type by @VChastinet
Expand Down
136 changes: 110 additions & 26 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@
"typescript": "^2.1.4",
"uglify-js": "^2.6.4"
}
}
}
45 changes: 31 additions & 14 deletions serializr.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ export interface Context {

export type Factory<T> = (context: Context) => T


export interface AdditionalPropArgs {
beforeDeserialize?: BeforeDeserializeFunc;
afterDeserialize?: AfterDeserializeFunc;
}

export interface PropSchema {
serializer(sourcePropertyValue: any): any;
deserializer(jsonValue: any, callback: (err: any, targetPropertyValue: any) => void, context: Context, currentPropertyValue: any): void;
beforeDeserialize: BeforeDeserializeFunc;
afterDeserialize: AfterDeserializeFunc;
}

export type Props = {
Expand All @@ -28,6 +36,10 @@ export interface ModelSchema<T> {
export type Clazz<T> = new(...args: any[]) => T;
export type ClazzOrModelSchema<T> = ModelSchema<T> | Clazz<T>;

export type AfterDeserializeFunc = (callback: (err: any, value: any) => void, err: any, newValue: any, jsonValue: any, jsonParentValue: any, propNameOrIndex: string | number, context: Context, propDef: PropSchema, numRetry: number) => void;

export type BeforeDeserializeFunc = (callback: (err: any, value: any) => void, jsonValue: any, jsonParentValue: any, propNameOrIndex: string | number, context: Context, propDef: PropSchema) => void;

export function createSimpleSchema<T extends Object>(props: Props): ModelSchema<T>;

export function createModelSchema<T extends Object>(clazz: Clazz<T>, props: Props, factory?: Factory<T>): ModelSchema<T>;
Expand All @@ -42,39 +54,44 @@ export function setDefaultModelSchema<T>(clazz: Clazz<T>, modelschema: ModelSche
export function serialize<T>(modelschema: ClazzOrModelSchema<T>, instance: T): any;
export function serialize<T>(instance: T): any;

export function cancelDeserialize<T>(instance: T): void;

export function deserialize<T>(modelschema: ClazzOrModelSchema<T>, jsonArray: any[], callback?: (err: any, result: T[]) => void, customArgs?: any): T[];
export function deserialize<T>(modelschema: ClazzOrModelSchema<T>, json: any, callback?: (err: any, result: T) => void, customArgs?: any): T;

export function update<T>(modelschema: ClazzOrModelSchema<T>, instance:T, json: any, callback?: (err: any, result: T) => void, customArgs?: any): void;
export function update<T>(instance:T, json: any, callback?: (err: any, result: T) => void, customArgs?: any): void;

export function primitive(): PropSchema;
export function primitive(additionalArgs?: AdditionalPropArgs): PropSchema;

export function identifier(registerFn?: (id: any, value: any, context: Context) => void): PropSchema;
export function identifier(registerFn?: (id: any, value: any, context: Context) => void, additionalArgs?: AdditionalPropArgs): PropSchema;
export function identifier(additionalArgs: AdditionalPropArgs): PropSchema;

export function date(): PropSchema;
export function date(additionalArgs?: AdditionalPropArgs): PropSchema;

export function alias(jsonName: string, propSchema?: PropSchema | boolean): PropSchema;

export function child(modelschema: ClazzOrModelSchema<any>): PropSchema;
export function object(modelschema: ClazzOrModelSchema<any>): PropSchema;
export function child(modelschema: ClazzOrModelSchema<any>, additionalArgs?: AdditionalPropArgs): PropSchema;
export function object(modelschema: ClazzOrModelSchema<any>, additionalArgs?: AdditionalPropArgs): PropSchema;

export type RefLookupFunction = (id: string, callback: (err: any, result: any) => void,context:Context) => void;
export type RegisterFunction = (id: any, object: any, context: Context) => void;

export function ref(modelschema: ClazzOrModelSchema<any>, lookupFn?: RefLookupFunction): PropSchema;
export function ref(identifierAttr: string, lookupFn: RefLookupFunction): PropSchema;
export function reference(modelschema: ClazzOrModelSchema<any>, lookupFn?: RefLookupFunction): PropSchema;
export function reference(identifierAttr: string, lookupFn: RefLookupFunction): PropSchema;
export function ref(modelschema: ClazzOrModelSchema<any>, lookupFn?: RefLookupFunction, additionalArgs?: AdditionalPropArgs): PropSchema;
export function ref(modelschema: ClazzOrModelSchema<any>, additionalArgs?: AdditionalPropArgs): PropSchema;
export function ref(identifierAttr: string, lookupFn: RefLookupFunction, additionalArgs?: AdditionalPropArgs): PropSchema;
export function reference(modelschema: ClazzOrModelSchema<any>, lookupFn?: RefLookupFunction, additionalArgs?: AdditionalPropArgs): PropSchema;
export function reference(modelschema: ClazzOrModelSchema<any>, additionalArgs?: AdditionalPropArgs): PropSchema;
export function reference(identifierAttr: string, lookupFn: RefLookupFunction, additionalArgs?: AdditionalPropArgs): PropSchema;

export function list(propSchema: PropSchema): PropSchema;
export function list(propSchema: PropSchema, additionalArgs?: AdditionalPropArgs): PropSchema;

export function map(propSchema: PropSchema): PropSchema;
export function map(propSchema: PropSchema, additionalArgs?: AdditionalPropArgs): PropSchema;

export function mapAsArray(propSchema: PropSchema, keyPropertyName: string): PropSchema;
export function mapAsArray(propSchema: PropSchema, keyPropertyName: string, additionalArgs?: AdditionalPropArgs): PropSchema;

export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context?: any, oldValue?: any) => any): PropSchema;
export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => any): PropSchema;
export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context?: any, oldValue?: any) => any, additionalArgs?: AdditionalPropArgs): PropSchema;
export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => any, additionalArgs?: AdditionalPropArgs): PropSchema;

export function serializeAll<T extends Function>(clazz: T): T

Expand Down
61 changes: 47 additions & 14 deletions src/core/Context.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { GUARDED_NOOP, once, invariant, isAssignableTo } from "../utils/utils"

var rootContextCache = new Map()

export default 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
this.target = null // always set this property using setTarget
this.hasError = false
this.modelSchema = modelSchema
if (this.isRoot) {
Expand All @@ -24,26 +26,30 @@ export default function Context(parentContext, modelSchema, json, onReadyCb, cus
Context.prototype.createCallback = function (fn) {
this.pendingCallbacks++
// once: defend against user-land calling 'done' twice
return once(function(err, value) {
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.
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("\", \"") +
"\""
))
else
this.onReadyCb(null, this.target)
"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))
Expand Down Expand Up @@ -71,7 +77,7 @@ Context.prototype.await = function (modelSchema, uuid, callback) {
}

// given a model schema, uuid and value, resolve all references that where looking for this object
Context.prototype.resolve = function(modelSchema, uuid, value) {
Context.prototype.resolve = function (modelSchema, uuid, value) {
invariant(this.isRoot)
if (!this.resolvedRefs[uuid])
this.resolvedRefs[uuid] = []
Expand All @@ -88,4 +94,31 @@ Context.prototype.resolve = function(modelSchema, uuid, 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
}

export function getTargetContext(target) {
return rootContextCache.get(target)
}
18 changes: 18 additions & 0 deletions src/core/cancelDeserialize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Deserialization
*/
import { invariant } from "../utils/utils"
import { getTargetContext } from "./Context"


/**
* Cancels an asynchronous deserialization or update operation for the specified target object.
* @param instance object that was previously returned from deserialize or update method
*/
export default function cancelDeserialize(instance) {
invariant(typeof instance === "object" && instance && !Array.isArray(instance), "cancelDeserialize needs an object")
var context = getTargetContext(instance)
if (context) {
context.cancelAwaits()
}
}
Loading

0 comments on commit e46f798

Please sign in to comment.