vuex-webextensions icon indicating copy to clipboard operation
vuex-webextensions copied to clipboard

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

Open Kocal opened this issue 6 years ago • 8 comments

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 { Channel } from '...';

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 avatar Jan 05 '19 07:01 Kocal

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;
      });
    }
  }
});

Kocal avatar Jan 05 '19 08:01 Kocal

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

MitsuhaKitsune avatar Mar 18 '19 01:03 MitsuhaKitsune

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 :stuck_out_tongue:)

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... :confused: 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. :slightly_smiling_face:

Kocal avatar Mar 18 '19 07:03 Kocal

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.

Kocal avatar Mar 18 '19 07:03 Kocal

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 avatar Mar 23 '19 19:03 MitsuhaKitsune

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 https://github.com/MitsuhaKitsune/vuex-webextensions/issues/17#issuecomment-473802209?

Kocal avatar Mar 23 '19 20:03 Kocal

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.

MitsuhaKitsune avatar Mar 23 '19 21:03 MitsuhaKitsune

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 ?

KBoyarchuk avatar May 20 '19 17:05 KBoyarchuk