Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vuex state with a Class inside is working in background, but not in popup #17

Open
Kocal opened this issue Jan 5, 2019 · 8 comments
Open
Labels
help wanted Extra attention is needed

Comments

@Kocal
Copy link

Kocal commented Jan 5, 2019

Hey! I finally have some time to use this Vuex plugin on a real project! :)

I think my problem is due to serialization/unserialization (JSON?) of my store state.

Given my store is:

import Vue from 'vue';
import Vuex from 'vuex';
import { Channel } from '...';

export const store = new Vuex.Store({
  state: {
    channels: [new Channel('foo', 'bar')]
  },
  getters: {
    firstChannel(state) {
      return state.channels[0];
    }
  }
})

When I use it in background, store.getters.firstChannel is an instance of Channel class.
But when I use it in my popup page inside a Vue component, this.$store.getters.firstChannel (or mapGetters(['firstChannel'])) is a plain object, without prototype.

I think it's a problem of serialization because some months ago, I had the same problem with chrome.runtime.sendMessage(). For each received data, I had to manually set __proto__ property:

<!-- in a .vue file -->
<script>
import { Channelfrom '...';

export default {
  data() {
    return {
      channels: []
    }
  },
  methods: {
    retrieveChannels() {
      chrome.runtime.sendMessage({ type: 'GET_CHANNELS' }, response => {
        this.channels = response.data.channels.map(channel => {
          channel.__proto__ = Channel.prototype;
          return channel;
        });
      });
    },
  },
}
</script>

But in this case I don't know how to do this.
Maybe a store watcher inside my popup/main.js or a Vue watcher inside popup/App.vue?

What do you think?

Thanks! :)

@Kocal Kocal changed the title How can I pass an ES6 Class/Prototype from Background to Popup? Vuex state with a Class inside is working in background, but not in popup Jan 5, 2019
@Kocal
Copy link
Author

Kocal commented Jan 5, 2019

Okay so I found a workaround, it's not really pretty but it works.

I'm using a computed property that use a store's getter, and set prototypes on the fly.

In my case, I do this:

<script>
import { Game } from '../entities/Game';
import { Stream } from '../entities/Stream';
import { Channel } from '../entities/Channel';

export default {
  computed: {
    twitchChannels() {
      return this.$store.getters.twitchChannels.map(channel => {
        channel.__proto__ = Channel.prototype;

        if (channel.stream) {
          channel.stream.__proto__ = Stream.prototype;

          if (channel.stream.game) {
            channel.stream.game.__proto__ = Game.prototype;
          }
        }

        return channel;
      });
    },
  }
}
</script>

EDIT: Something even better, make the getter setting prototypes on the fly (so we only do this only ONE time):

import { Game } from '../entities/Game';
import { Stream } from '../entities/Stream';
import { Channel } from '../entities/Channel';

export new Vuex.Store({
  state: {
    twitchChannels: [new Channel(...)],
  },
  getters: {
    twitchChannels(state) {
      return state.twitchChannels.map(channel => {
        channel.__proto__ = Channel.prototype;
        if (channel.stream) {
          channel.stream.__proto__ = Stream.prototype;
          if (channel.stream.game) {
            channel.stream.game.__proto__ = Game.prototype;
          }
        }

        return channel;
      });
    }
  }
});

@MitsuhaKitsune
Copy link
Owner

Hey @Kocal, nice to see you here ^^

In first place thanks for the report and sorry for the delay, I'm passing on a very busy season and I can't answer before.

It's the first time that I see a Vuex state storing a class instance, so I can't help so much before mount this enviroment and test it.

The thing are amazing, didn't know that this are possible before, I think that the problem are on the serialization of data on the webextension message, this probably it's breaking the class object at some point.

Are your project opensource to test it? If not don't worry, I can mount a simple enviroment to test this issue.

Greetings

@Kocal
Copy link
Author

Kocal commented Mar 18, 2019

Hey, don't worry, it can happen to anyone :)

Yes I'm regularly using classes a DTO because I can implement some useful methods on them.

And since I need to keep my objects synchronized between the background part and the popup part, I have to store them in Vuex (alongside your plugin 😛)

I didn't checked the plugin source code, but I suppose you serialize data with JSON.stringify() and unserialize them with JSON.parse?
Sélection_020

My trick is working because I can import my classes and then update __proto__ prop, but I don't think we will be about to do something in the plugin... 😕
Capture d'écran de 2019-03-18 08-34-38

My project is not opensourced (it's a project for a client), it's inside a private repo but I can add you as a collaborator. 🙂

@Kocal
Copy link
Author

Kocal commented Mar 18, 2019

Or maybe we can manually register prototypes:

import { Channel } from './entities/Channel';
import { Game } from './entities/Game';
import { Stream } from './entities/Stream';

export new Vuex.Store({
  plugins: [
    VuexWebExtensions({
      serializationPrototypes: {
        Channel,
        Game,
        Stream,
      }
    })
  ],
});

Before serializing:

  • we recursively iterate on the state to find objects that have a prototype different from Object
  • in this object, we store the name of the prototype under a private property, something like: $__PROTOTYPE_NAME__$ (to be sure that will never conflict with the user's classes)

After unserializing:

  • we recursively iterate on the unserialized data, trying to find $__PROTOTYPE_NAME__$ prop
  • when we found it, we can set the prototype like this: obj.__proto__ = this.serializationPrototypes[obj.$__PROTOTYPE_NAME__$].prototype

I think it can works, but we should be careful about nested objects.

@MitsuhaKitsune
Copy link
Owner

Hi again @Kocal, after some days researching for this, can't offer any solution for now.

I don't apply any serialization (the browser do it automatically), I try some things, only one work but it's so dangerous.

The unic way that I found to restore class automatically with the plugin, it's jsonify the class methods as plain text, restore it with eval and then restore the values, it is the only thing that work for now but I can't implement it because security reasons.

Eval are so dangerous and the review team of any browser gona reject the extensions that use it on any part.

I don't have any more things for now, the best way that I see to restore the class, it's create new instance and then merge values with it but I can't do this on the plugin, you should import the class and create new instance manually.

I think the best way it's add proto inside computed property, if you have any more things just say to me to try,

@MitsuhaKitsune MitsuhaKitsune added the help wanted Extra attention is needed label Mar 23, 2019
@Kocal
Copy link
Author

Kocal commented Mar 23, 2019

Hi 👋

I don't apply any serialization (the browser do it automatically), I try some things, only one work but it's so dangerous.

Ah yes, I forgot that you use sendMessage() method that automatically serialize data.

The unic way that I found to restore class automatically with the plugin, it's jsonify the class methods as plain text, restore it with eval and then restore the values, it is the only thing that work for now but I can't implement it because security reasons.

And what about #17 (comment)?

@MitsuhaKitsune
Copy link
Owner

This can be working I guess, it need a optional extra initialization and a serializer class to compare data structure of specified classes and assign his prototype, but can work I guess.

I gona prepare and test this think and I come here with the feedback and the feature if it works.

@KBoyarchuk
Copy link
Contributor

Or maybe we can manually register prototypes:

import { Channel } from './entities/Channel';
import { Game } from './entities/Game';
import { Stream } from './entities/Stream';

export new Vuex.Store({
  plugins: [
    VuexWebExtensions({
      serializationPrototypes: {
        Channel,
        Game,
        Stream,
      }
    })
  ],
});

Before serializing:

  • we recursively iterate on the state to find objects that have a prototype different from Object
  • in this object, we store the name of the prototype under a private property, something like: $__PROTOTYPE_NAME__$ (to be sure that will never conflict with the user's classes)

After unserializing:

  • we recursively iterate on the unserialized data, trying to find $__PROTOTYPE_NAME__$ prop
  • when we found it, we can set the prototype like this: obj.__proto__ = this.serializationPrototypes[obj.$__PROTOTYPE_NAME__$].prototype

I think it can works, but we should be careful about nested objects.

What level of nested structure depth should be cloned ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants