knockout-postbox icon indicating copy to clipboard operation
knockout-postbox copied to clipboard

When publishing a value with a circular reference, a 'cyclic object value' error is thrown

Open stijnherreman opened this issue 8 years ago • 14 comments

When publishing a value with a circular reference, a 'cyclic object value' error is thrown. This is because of the serialization that happens to fill topicCache.

Code to reproduce:

const foo = {
  id: "foo",
  bar: ko.observable()
}
const bar = {
  id: "bar",
  foo: ko.observable()
}
ko.postbox.subscribe("foo", function() {
  console.log("foo was published");
})

ko.postbox.publish("foo", foo); // console: foo was published

foo.bar(bar);
ko.postbox.publish("foo", foo); // console: foo was published

bar.foo(foo);
ko.postbox.publish("foo", foo); // TypeError: cyclic object value

Is this something that can be solved by the library, or is it something I need to solve in my code?

stijnherreman avatar Sep 01 '16 11:09 stijnherreman

I'm currently using this workaround:

const serializer = ko.postbox.serializer;
ko.postbox.serializer = (object: any) => { return undefined; }
ko.postbox.publish("selectAdministration", this.administration);
ko.postbox.serializer = serializer;

I'm not using subscribeTo(...), or subscribe(...) with initializeWithLatestValue set to true, so I don't think I'm breaking anything.

stijnherreman avatar Sep 01 '16 12:09 stijnherreman

As you can see in this fiddle, the exception has nothing to do with knockout.postbox but with serialization.

https://jsfiddle.net/qqqy2q43/

My opinion is to fix it in your code as the only way to "fix" this is to replace/remove serialization as you are already doing. Luckily postbox allows you to replace the serialization, which defaults to ko.toJSON() but as you can see in my fiddle the bug exists aswel in JSON.stringify().

Btw, ko.toJSON() actually uses JSON.stringify() as you can see here:

https://github.com/knockout/knockout/blob/master/src/subscribables/mappingHelpers.js#L20 https://github.com/knockout/knockout/blob/master/src/utils.js#L534

This kind of code is going to throw an exception in alot of libraries around there tho. Looking at the above links I would even guess knockout itself is not going to like this at some point.

Note: I Got nothing to do with this library, just my 2 cents :-D

frederikprijck avatar Sep 01 '16 13:09 frederikprijck

As you can see in this fiddle, the exception has nothing to do with knockout.postbox but with serialization.

I'm aware of the technical details and understand them, but one could argue that the serialization happening here is an implementation detail of knockout-postbox, since I had to dig into the code to understand where the error came from.

stijnherreman avatar Sep 01 '16 14:09 stijnherreman

The best thing to do in order to understand a library is always to dig in the code, so no issue there tbh.

The serialization happening here is totally unrelated from postbox tho, as you can tell by looking at the source code. One could argue that a library, which extends knockout, should be using the same serialization as knockout itself, which postbox is doing.

So ye, on that part one could argue that knockout could be fixing this. But then again I guess you will be pointed to the JS specs as JSON serialization doesn't support circular references (but hey, why would it need to serialize the exact same 2 objects over and over again ?).

On the other hand, I wouldn't realy want a library to support your situation for serialization as you have a circular reference which would lead to a never ending (and useless) serialization.

If you look at http://stackoverflow.com/questions/10392293/stringify-convert-to-json-a-javascript-object-with-circular-reference they have a workaround which involves adapting the serialization in such a way that it excludes the circular references. Imho this is a lot of effort for something that could have been avoided by not passing in an object with circular referencing.

Not that it's my business, but why are you using this circular references ?

frederikprijck avatar Sep 01 '16 14:09 frederikprijck

Not that it's my business, but why are you using this circular references ?

We have a UI built with components (hence the use of knockout-postbox, for communication between them), where certain actions can link two ~~viewmodels~~ data models together and then they sometimes need to access each other's data. There are likely other ways to tackle this without circular references, but this seemed like the easiest and most logical implementation to us.

stijnherreman avatar Sep 01 '16 15:09 stijnherreman

Using circular references will most likely never be the easiest solution in a language where serializing those objects is not supported.

On Sep 1, 2016 17:02, "Stijn Herreman" [email protected] wrote:

Not that it's my business, but why are you using this circular references ?

We have a UI built with components (hence the use of knockout-postbox, for communication between them), where certain actions can link two viewmodels together and then they sometimes need to access each other's data. There are likely other ways to tackle this without circular references, but this seemed like the easiest and most logical implementation to us.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rniemeyer/knockout-postbox/issues/42#issuecomment-244107420, or mute the thread https://github.com/notifications/unsubscribe-auth/ACDCV0uV74qJMNsyxD-12gWzrJlyxmJjks5qluj0gaJpZM4Jyl3a .

frederikprijck avatar Sep 01 '16 15:09 frederikprijck

Btw u are using postbox for losely coupling up components yet u bind up view models directly to each other. Imho not a good idea as u are breaking loosely coupling.

Not rly related but I just wanted to mention it.

frederikprijck avatar Sep 01 '16 15:09 frederikprijck

Aparantly I can't edit my posts on mobile, so sorry for the amount of replies.

Imho if u want a more clean solution then urs I would go for :

  • create a ko extension method subscribeWithoutSerialization which allows u for reusing it and even sharing it with the community.
  • create a library (e. g. Json.parse.circular.js) and override json stringify and parse to handle ur case. But u can never entirely serialize or deserialize circular references without stopping the circular referencing at some lever. Ur stack will explode if u try to

frederikprijck avatar Sep 01 '16 15:09 frederikprijck

I would go for PR with a change like this:

    exports.defaultCacheInitializer = function(value) {
        return {
            value: value,
            serialized: exports.serializer(value)
        };
    }
    exports.cacheInitializer = exports.defaultCacheInitializer;

    //wrap notifySubscribers passing topic first and caching latest value
    exports.publish = function(topic, value) {
        if (topic) {
            //keep the value and a serialized version for comparison
            exports.topicCache[topic] = exports.cacheInitializer(value);
            exports.notifySubscribers(value, topic);
        }
    };

So you can just get rid of all the serialization for initializing and comparing cache values if it does no suit your needs.

By the way, knockout.postbox is amazing, thanks for that.

fqborges avatar Sep 01 '16 15:09 fqborges

That might be a bit cleaner then overriding the serializer but the effect is comparable to how @stijnherreman is overriding the serializer ?

frederikprijck avatar Sep 01 '16 16:09 frederikprijck

I think that in practice the effect is the same, using the defaults will still throw the serialization error and to avoid it you need to override something. But it will make the library not dependent in serialization anymore, as it is only related to the default cache comparison, and it could be replaced with something else, like timestamps or ids.

Also, it could make easier to create cache related extensions to ko.postbox.

fqborges avatar Sep 01 '16 17:09 fqborges

Well, something came up now. The serialization concept (cached.serialized specifically) could be change to something like a 'hash' or 'memento' or 'identifier'. I mean, something that could be used to compare values like a Object#equals in java. Thinking about it it is used only as a optimization to avoid running serialized(cached.value) over and over.

fqborges avatar Sep 01 '16 17:09 fqborges

Might not be interesting, but when I started to use this library in the past days I simply assigned the serializer an identity function like

ko.postbox.serializer = function(value) { return value };

because I actually don't need the caching and the notification with latest value feature currently.

cervengoc avatar Nov 24 '16 14:11 cervengoc

Well eventually you might hit a point where you do need to serialize the object which includes a circular reference, and removing the serializer won't help in that situation.

If that's the case, you may want to look into Douglas Crockford's cycle.js: https://github.com/douglascrockford/JSON-js

For KO postbox, you could easily create a wrapper around cycle.js and use it to serialize and deserialize your objects containing circular references.

frederikprijck avatar Nov 25 '16 07:11 frederikprijck