Skip to content

Commit

Permalink
Added whenDone function parameter to the unDry & unDryFunction
Browse files Browse the repository at this point in the history
…function calls
  • Loading branch information
skerit committed Oct 30, 2020
1 parent cc55830 commit 95279f0
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 17 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.1.0 (WIP)

* Added `whenDone` function parameter to the `unDry` & `unDryFunction` function calls
* Fixed infinite loop issue when using `Dry.clone()` that relies on `unDry` methods

## 1.0.12 (2019-11-22)

* Simple version boost to fix NPM publish issue
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,34 @@ undried.fullname();
// returns "Jelle De Loecker"
```

## Serializing & reviving instances with circular references

Some classes contain references to each other, for example:

```js
let alpha = new Alpha(),
beta = new Beta();

alpha.beta = beta;
beta.alpha = alpha;
```

The problem is that when you serialize & then try to revive this, one of the `unDry` methods will receive an un-revived placeholder. This can obviously cause issues, especially when setting the property has side-effects. So a new argument `whenDone` has been added to the `unDry` method, like so:

```js
Alpha.prototype.unDry = function unDry(obj, custom_method, whenDone) {

let alpha = new Alpha();

whenDone(function() {
alpha.beta = obj.beta;
});

return alpha;
}
```

`whenDone` functions will be called just before the `Dry.undry()` function exits, so all the references will have been revived by then.

### toObject

Expand Down
73 changes: 62 additions & 11 deletions lib/json-dry.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ function generateReviver(reviver, undry_paths) {
*
* @author Jelle De Loecker <[email protected]>
* @since 1.0.0
* @version 1.0.10
* @version 1.1.0
*
* @param {Object} obj
* @param {String} custom_method Custom method to use if available
Expand Down Expand Up @@ -466,19 +466,52 @@ function clone(obj, custom_method, extra_args, wm) {
return wm.get(obj);
}

if (!wm.when_done_queue) {
wm.when_done_queue = [];
wm.drymap = new Map();

wm.whenDone = function whenDone(fnc) {
if (!fnc) return;
wm.when_done_queue.push(fnc);
};
}

if (custom_method) {
extra_args = [wm].concat(extra_args);
}

return real_clone({'_': obj}, custom_method, extra_args, wm)['_'];
let result = real_clone({'_': obj}, custom_method, extra_args, wm)['_'],
i;

if (wm.when_done_queue.length) {
let temp_val,
key;

// Iterate over all the objects created with the `toDry` method
wm.drymap.forEach(function each(val, temp) {
for (key in temp) {
temp_val = temp[key];

if (wm.drymap.has(temp_val)) {
temp[key] = wm.drymap.get(temp_val);
}
}
});

for (i = 0; i < wm.when_done_queue.length; i++) {
wm.when_done_queue[i]();
}
}

return result;
}

/**
* Deep clone an object
*
* @author Jelle De Loecker <[email protected]>
* @since 1.0.0
* @version 1.0.10
* @version 1.1.0
*
* @param {Object} obj
* @param {String} custom_method Custom method to use if available
Expand Down Expand Up @@ -548,19 +581,27 @@ function real_clone(obj, custom_method, extra_args, wm) {
target[key] = entry.dryClone(wm, custom_method);
} else if (entry.toDry) {
// Perform the toDry function
temp = entry.toDry();
let uncloned_dried = entry.toDry();

// Remember this temporary object to prevent infinite loops
wm.set(entry, uncloned_dried);

// Clone the value,
// because returned objects aren't necesarilly cloned yet
temp = real_clone(temp, custom_method, extra_args, wm).value;
let cloned_dried = real_clone(uncloned_dried, custom_method, extra_args, wm).value;

// Perform the undry function
if (entry.constructor.unDry) {
target[key] = entry.constructor.unDry(temp, custom_method || true);
target[key] = entry.constructor.unDry(cloned_dried, custom_method || true, wm.whenDone);
} else {
// If there is no undry function, the clone will be a simple object
target[key] = temp;
target[key] = cloned_dried;
}

// Remember both entries (for circular undry references)
wm.drymap.set(cloned_dried, target[key]);
wm.drymap.set(uncloned_dried, target[key]);

} else if (name_type == 'Date') {
target[key] = new Date(entry);
} else if (name_type == 'RegExp') {
Expand Down Expand Up @@ -1171,7 +1212,7 @@ function walk(obj, fnc, result, seen) {
*
* @author Jelle De Loecker <[email protected]>
* @since 1.0.0
* @version 1.0.11
* @version 1.1.0
*
* @param {Object} value
*
Expand All @@ -1180,6 +1221,7 @@ function walk(obj, fnc, result, seen) {
function parse(object, reviver) {

var undry_paths = new Map(),
when_done = [],
retrieve = {},
reviver,
result,
Expand Down Expand Up @@ -1209,6 +1251,11 @@ function parse(object, reviver) {
return result;
}

function whenDone(fnc) {
if (!fnc) return;
when_done.push(fnc);
}

// To remember which objects have already been revived
seen = new WeakMap();

Expand All @@ -1225,9 +1272,9 @@ function parse(object, reviver) {
regenerate(result, null, entry, seen, retrieve, undry_paths, old, path_array.slice(0));

if (entry.unDryConstructor) {
entry.undried = entry.unDryConstructor.unDry(entry.value, false);
entry.undried = entry.unDryConstructor.unDry(entry.value, false, whenDone);
} else if (entry.unDryFunction) {
entry.undried = entry.unDryFunction(entry, null, entry.value);
entry.undried = entry.unDryFunction(entry, null, entry.value, whenDone);
} else {
entry.undried = entry.value;
}
Expand Down Expand Up @@ -1286,7 +1333,11 @@ function parse(object, reviver) {
result = regenerate(result, result, result, seen, retrieve, undry_paths, old, []);

if (result.undried != null && result.dry) {
return result.undried;
result = result.undried;
}

for (i = 0; i < when_done.length; i++) {
when_done[i]();
}

return result;
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "json-dry",
"description": "Don't repeat yourself, JSON: Add support for (circular) references, class instances, ...",
"version": "1.0.12",
"version": "1.1.0-alpha",
"author": "Jelle De Loecker <[email protected]>",
"keywords": [
"json",
Expand All @@ -12,15 +12,15 @@
"main": "./lib/json-dry.js",
"repository": "[email protected]:skerit/json-dry.git",
"scripts": {
"test" : "node_modules/.bin/mocha --reporter spec",
"test" : "mocha --exit --reporter spec --bail --timeout 5000 --file test/dry.js",
"coverage" : "./node_modules/istanbul/lib/cli.js cover _mocha",
"report-coverage" : "cat ./coverage/lcov.info | coveralls"
"report-coverage" : "codecov"
},
"license": "MIT",
"devDependencies": {
"coveralls" : "^2.11.6",
"istanbul" : "^0.4.5",
"mocha" : "1.20.x",
"codecov" : "~3.7.0",
"nyc" : "^15.1.0",
"mocha" : "~8.0.1",
"protoblast" : "skerit/protoblast#3218106"
},
"engines": {
Expand Down
105 changes: 105 additions & 0 deletions test/dry.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,111 @@ describe('Dry', function TestDry() {
assert.strictEqual(+undried[1].options.b.options.date, +date);
assert.strictEqual(+undried[3], +date, 'The second date reference was not undried correctly');
});

it('should handle circular references in unDry methods', function() {

let attempt = 0;

function Perang(name) {
this.name = name;
this.refleep = null;
};

Perang.unDry = function unDry(obj, force, whenDone) {

let result = new Perang(obj.name);

result.refleep = obj.refleep;

if (attempt == 0) {
// Next call will be for the clone test
assert.strictEqual(obj.refleep.dry, 'toDry', 'The dried variant of the class is expected');
}

attempt++;

whenDone(function setRefleep() {
result.refleep = obj.refleep;

assert.strictEqual(obj.refleep instanceof Refleep, true, 'The `refleep` property should have been undried by now')
});

return result;
};

Perang.prototype.toDry = function toDry() {
return {
value: {
name : this.name,
refleep : this.refleep
}
}
};

function Refleep(name) {
this.name = name;
this.perang = null;
}

Refleep.unDry = function unDry(obj, force, whenDone) {

let result = new Refleep(obj.name);

whenDone(function setRefleep() {
result.perang = obj.perang;
});

return result;
};

Refleep.prototype.toDry = function toDry() {
return {
value: {
name : this.name,
perang : this.perang
}
}
};

Dry.registerClass(Perang);
Dry.registerClass(Refleep);

// This test depends on the order!
let perang = new Perang('perang-1');
let refleep = new Refleep('refleep-1');

perang.refleep = refleep;
refleep.perang = perang;

let serialized = Dry.toObject([refleep, perang]);
let unserialized = Dry.parse(serialized);

let perang_two = unserialized[1],
refleep_two = unserialized[0];

assert.strictEqual(perang !== perang_two, true);
assert.strictEqual(refleep !== refleep_two, true);

assert.strictEqual(perang_two, refleep_two.perang);
assert.strictEqual(refleep_two, perang_two.refleep);

assert.strictEqual(perang_two instanceof Perang, true);
assert.strictEqual(refleep_two instanceof Refleep, true);

let cloned = Dry.clone([refleep, perang]);

perang_three = cloned[1];
refleep_three = cloned[0];

assert.strictEqual(perang !== perang_three, true);
assert.strictEqual(refleep !== refleep_three, true);

assert.strictEqual(perang_three, refleep_three.perang);
assert.strictEqual(refleep_three, perang_three.refleep);

assert.strictEqual(perang_three instanceof Perang, true);
assert.strictEqual(refleep_three instanceof Refleep, true);
});
});

describe('.toObject()', function() {
Expand Down

0 comments on commit 95279f0

Please sign in to comment.