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-multi-tab-state and vuex-persistedstate #63

Open
AndreiSoroka opened this issue Oct 13, 2021 · 11 comments
Open

vuex-multi-tab-state and vuex-persistedstate #63

AndreiSoroka opened this issue Oct 13, 2021 · 11 comments

Comments

@AndreiSoroka
Copy link

Hi. I simultaneously usage vuex-multi-tab-state and vuex-persistedstate.
Both usage localstorage.

Does vuex-multi-tab-state completely replace vuex-persistedstate functionality?

thanks

@wparad
Copy link

wparad commented Oct 13, 2021

It's worse than duplicate storage, there is no way to know when the user first opens the site. Arguably the functionality should be:

When the last tab is closed, vuex-multi-tab-state wipes it cache.

From a practical standpoint this would actually mean, setting some sort of initialization flag to populate the state from the vuex store and then using it from that point on, until the vuex store resets itself. Perhaps it is as simple as setting a flag in vuex (making sure it is never persisted) and then checking the flag before loading/overwriting the state. (and then setting the flag)

@AndreiSoroka
Copy link
Author

Temporrary, I always remove from localstorage when user is open new tab

window.localStorage.removeItem('vuex-multi-tab');

Each tab register two modules (vuex-multi-tab-state, vuex-persistedstate)

And it is works. (First tab work with recreated vuex-multi-tab)

But I think it is temporrary solve... I'm afraid this "bike" will break...

@wparad
Copy link

wparad commented Oct 13, 2021

That's a great first start, but I think you'll also need to handle resuming a sleeping tab as well.

@gabrielmbmb
Copy link
Owner

Hey, thank you for submitting this issue. I'll put a boolean flag in the plugin initialization indicating whether or not the content in the local storage should be removed after the last tab has been closed. What do you think?

@wparad
Copy link

wparad commented Oct 18, 2021

Its the right direction, but the problem is there is no reliable way to know when a tab is closed. For instance if the browser is closed itself or all tabs are put into the background, it would need to be treated the same but no tabs have actually been closed. Maybe more realistic would be an initialize/enable method, or even better an injected computed property that causes the plugin to clone the current store, wipe its cache and start syncing the in memory store.

I think it has to continually check "if enabled" as that state can change even if the tab isn't closed. It's probably sufficient with a method, as long as the value isn't persisted. Though I don't see a straightforward way to do that. So the computed property reactivity check is probably the only way.

@AndreiSoroka
Copy link
Author

Yes. Need research first initialization.

How I see:

If I inited plugin with argument: {autoClearCache: true} then:

  1. Open new tab
  2. Remove cache from localstorage
  3. Init vuex
  4. Add to localstorage

If I have already opened tab:

  1. Listen localstorage (If somebody remove and add localstorage, tab is getting event)
  2. If somebody change vuex and localstorage is empty > just create

@KaskoYurii
Copy link

I have the same issue:
New changes in the store do not appear until I have not reset the all browser cache.
If I will clear localStorage on the tab opening, I'm not sure that this solution will work while I just reload my tab.
Maybe there is an updated solution?

@cepm-nate
Copy link

I think we need a new solution wherein the Vuex store is actually entirely in a shared worker, and then every tab that opens creates a mirrored store which sends all it's actions, mutations, and getters to the shared worker store, and the shared worker store does all the real work, then emits an state changes out to the mirrored stores. I use "mirrored" here, but in reality they are more like pass-through stores. That way the 'persisted' plugin would run on the shared worker store, and it wouldn't matter when you opened a tab or how many...etc. I've started working on such a setup, but would be very happy to see someone else officially create such a thing. Anyone know of someone else who's tried it?

@wparad
Copy link

wparad commented Aug 19, 2024

I think we need a new solution wherein the Vuex store is actually entirely in a shared worker, and then every tab that opens creates a mirrored store which sends all it's actions, mutations, and getters to the shared worker store, and the shared worker store does all the real work, then emits an state changes out to the mirrored stores. I use "mirrored" here, but in reality they are more like pass-through stores. That way the 'persisted' plugin would run on the shared worker store, and it wouldn't matter when you opened a tab or how many...etc. I've started working on such a setup, but would be very happy to see someone else officially create such a thing. Anyone know of someone else who's tried it?

That's exactly the right solution to have. It could be challenging figuring out the polling window, I wonder if there are any browser apis for this, or if some sort service worker is the recommended solution. It wouldn't be great constantly be grabbing all the data from localstore every millisecond, I can't even imagine this is a Vue problem. Literally every SPA and MPA has this issue.

@cepm-nate
Copy link

I've implemented a solution which works, but is a little bit messy.

  • getters, actions, and mutations are all in js files.
  • SharedWorker implements Vue with Vuex, imports getters, actions, mutations. Also sets up modules and state.
  • VuexWrapper is in charge of creating the SharedWorker and communicating with it.
  • In main app, I set up the store with a couple local-only state properties.
  • In the main app store, I have functions which overwrite the calls for mutations, and actions, and send them to VuexWrapper to go to the shared worker. (getters are first attempted locally, and if function is not found, then sent to SharedWorker. This is for the Vuex Modules.)
  • Some of the store is local only (this would be per-tab stuff), as well as an 'isLoaded' state to track when initial state has been mirrored to the local store.
  • In the SharedWorker, I dynamically create watchers for all rootState properties (that's what Vue is for in the SharedWorker), and send updates to the main thread.
  • No polling needed, as all mutations are done on the SharedWorker store, and all UPDATES from those mutations get sent to all browser tabs.

It's messy for the following reasons:

  • Any getters that are in modules and accessed from the main app need to be async (messages to/from SharedWorkers are async). Root getters can be normal, as long as they are only accessing root state.
  • The entire store is mirrored to the local tab so that regular getters can do non-async retrievals. However, I'm not familiar with the internals of how setting x=y works when y came from the SharedStorage. Is it ByRef, or ByVal? Possible memory issues here.
  • Tracking which getters are async and which are not is complex.
  • The getters in getters.js can be called from either the main app, or from SharedStorage (getters calling each other or actions calling getters). This is usually not a problem, but if the getters access other services (like a LokiJS Database), then there might be issues with two LokiJS database persisting to the same IndexedDB storage keys.
  • Tracking when the initial store is mirrored to the tab (and holding off any possible store mutations/actions till then) is little fun.
  • Getting webpack to work with SharedWorkers is also little fun.

That's why an OFFICIAL SharedStorage plugin would be neat. Especially if it integrated with building tools, and handled all the communications automatically.

@cepm-nate
Copy link

I've refined my approach to be the following:

  • The state in the SharedWorker is the single source of truth.
  • I've created a "mutationsDependencyMap" file that is a large map showing which mutation effects which state property.
  • I've created a "getterDependencyMap" file that is a large map show which state properties are utilized in each getter.
  • I've modified the getters proxy, so when the getter is called, it utilizes the above map and adds those properties as local dependencies (in each tab), so the local Vuex instance can see the getter is tied to the state properties. THEN it calls the shared-worker getter.
  • In the Shared Worker, I removed the need for a Vue instance.
  • In the Shared Worker, I added a "mutationWatcher" that subscribes to the store mutation events. When one happens, it uses the mutationDependencyMap and emits a StateKeyUpdate message to all the tabs, so they in turn update their "local store", which then causes their getters to refresh.
  • Note on above: I don't send the whole mutation value to the store, just the signal that it updated. On the local (per tab) side I update the state property to a random value, so it triggers the getters that were dynamically tied to it. I use a random value in each state property so the entire object remains rather small (Some of my actual state properties are large tables)

Biggest downside of this entire setup is having to swap all store.state references in the main app (per tab) to be asyncComputed getters. This makes the app a little bit slower, as it now does an initial render, then re-renders the DOM as each asyncComputed property resolves. Prior to this swapout, all the computer properties directly depending on store.state values would be calculated at once, then the DOM would be generated.

If anyone has ideas on how to make my approach work better or would like to see snippets of code, let me know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants