bloc
bloc copied to clipboard
Hydrated bloc state lost after deploying updates to web app
Description Whenever I deploy an update to my web app, the bloc state is lost.
Steps To Reproduce
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: HydratedStorage.webStorageDirectory,
);
- Deploy the web app. (e.g. the counter example)
- Open the app and make changes to the state in the app (e.g. increment a counter)
- Deploy a new version of the app.
- Make sure the your using the updated version of the app.
- The state from step 2 is lost.
Expected Behavior The state should persist no matter if push an update.
I'm guessing this is because the HydratedStorage.webStorageDirectory is just cache which gets deleted on an update. But I would expect the default webStorageDirectory to persist.
Additional Context I'm using hydrated bloc to persist the theme mode, language settings as well as recently viewed items.
Hi @JakesMD š Thanks for opening an issue!
Does the issue persist when you upgrade to the latest stable version of hydrated_bloc (v10.0.0)?
Hi! Good point! I missed the update you released a couple hours ago. Will get back asap...
I works! š„³ Thanks!
Sorry š I just released another update to my app and the state was lost again.
Tomorrow I'll have a go at hosting the counter example app and seeing if I can reproduce the issue that way. That would prove whether this really is a hydrated_bloc issue.
How are you deploying updates? Sorry for the inconvenience!
Hi! Can confirm: this is reproducible with the counter example. You tend to lose state when you include a new package.
Steps to reproduce
- Create/clone the hydrated_bloc example.
- Create a Firebase hosting project and link the app to it.
- Build the release app with
flutter clean && flutter build web --release. - Deploy the app with
firebase deploy. - Open the deployed app in your browser and press some buttons.
- Repeat x times:
- Bump the app version in pubspec.yaml.
- Rename the app bar title to
Counter [VERSION]. - Build the release app with
flutter clean && flutter build web --release. - Deploy the app with
firebase deploy. - Reload the deployed app until your browser finally runs the new version with the changed app bar title.
That works perfectly. The state is always persisted.
Now do this:
- Bump the app version in pubspec.yaml.
- Rename the app bar title to
Counter [VERSION]. - Add a new package to pubspec.yaml. I used flutter_confetti.
- Add the new package somewhere in the app. I did this:
FloatingActionButton( child: const Icon(Icons.add), onPressed: () { Confetti.launch(context, options: const ConfettiOptions( particleCount: 100, spread: 70, y: 0.6)); context.read<CounterBloc>().add(CounterIncrementPressed()); }, ), - Build the release app with
flutter clean && flutter build web --release - Deploy the app with
firebase deploy - Reload the deployed app until your browser finally runs the new version with the changed app bar title.
Voila, your state was lost. Or at least parts of it. When I added flutter_confetti I lost the brightness, when I add signed_spacing_flex I lost all the state.
I tested this on MacBook Safari and Android Chrome.
@JakesMD thanks for the update and detailed reproduction steps! Are you able to share the Flutter version you're using? I'll try to reproduce shortly š
Stable 3.27.1
Hi! I've been playing around and have discovered some important points:
- The issue occurs after a significant change to the widget tree (more than just changing the app bar title)
- You can make the app fetch OLD state:
- Increment the counter to 6 and set brightness to dark
- Duplicate one of the floating action buttons, update, build and deploy
- The counter is now 0 and the brightness is light
- Increment the counter a few times to change the state
- Remove the duplicate floating action button, update, build and deploy
- The counter is now 6 again and brightness is dark
Hi! I've been playing around and have discovered some important points:
The issue occurs after a significant change to the widget tree (more than just changing the app bar title)
You can make the app fetch OLD state:
- Increment the counter to 6 and set brightness to dark
- Duplicate one of the floating action buttons, update, build and deploy
- The counter is now 0 and the brightness is light
- Increment the counter a few times to change the state
- Remove the duplicate floating action button, update, build and deploy
- The counter is now 6 again and brightness is dark
Thanks for all the context! I'm digging into this now š
I was able to reproduce and I was able to resolve this by changing my firebase hosting configuration to use:
"public": "build/web",
instead of
"source": "."
This way the firebase-deploy command doesn't actually run the flutter build step. Then I ran flutter build web --pwa-strategy=none to disable the service worker and I was able to add new packages without losing my cached state.
Let me know if that helps and apologies for the delayed response!
Seems to be fixed š It works when you make different changes to the app on every update (which will be 99% of the time). But when you revert changes it loads old state again.
My firebase config was already set to "public": "build/web".
After building with --pwa-strategy=none I was unable to make the app load the new version. So I set the max cache age to 60s in firebase.json:
"headers": [
{
"source": "**",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=60"
}
]
}
]
I've only tested this with the counter example and have yet to try it with the production app.
Seems to be fixed š It works when you make different changes to the app on every update (which will be 99% of the time). But when you revert changes it loads old state again.
My firebase config was already set to
"public": "build/web".After building with
--pwa-strategy=noneI was unable to make the app load the new version. So I set the max cache age to 60s infirebase.json:"headers": [ { "source": "**", "headers": [ { "key": "Cache-Control", "value": "max-age=60" } ] } ] I've only tested this with the counter example and have yet to try it with the production app.
Can you try unregistering the service worker manually? I need to dig into what the generated service worker is doing but Iād check if a brand new app/deployment has the same issues.
Can you try the code here and see if anything changes?
https://github.com/IO-Design-Team/hive_ce/pull/74
Hi! Sorry guys, I missed your last few comments. And I'm afraid --pwa-strategy=none didn't work the last time we released an update...
Glad this issue is getting attention; I'm experiencing the same problem.
I thought the issue was related to IndexedDB/Hive and was planning on making my own implementation of your Storage interface using something like SharedPreferences. Sounds like you've recently ruled that out, however.
I don't know enough about the service worker to know if this issue would still persist if I were to swap HydatedStorage.build() for a custom SharedPreferencesStorage.
I'll keep an eye on this thread. If there's anything you'd like me to try on my site, let me know.
edit: Updating to below works: [Breaking change] HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), );
Any updates on this? This issue is preventing me from deploying updates to the two websites that are my livelihood, and each workaround I've tried has not worked.
The last workaround I tried was creating my own Storage implementation using SharedPreferences if kIsWeb is true, and using the state constructor parameter of my Cubits/Blocs to manually bring in the value from SharedPreferences. Alas, whatever's going on with generated service worker still wiped the data.
Also I'm not sure if the previous comment from bharathwajv has anything to do with this particular issue.
Any updates on this? This issue is preventing me from deploying updates to the two websites that are my livelihood, and each workaround I've tried has not worked.
The last workaround I tried was creating my own Storage implementation using SharedPreferences if
kIsWebis true, and using thestateconstructor parameter of my Cubits/Blocs to manually bring in the value from SharedPreferences. Alas, whatever's going on with generated service worker still wiped the data.Also I'm not sure if the previous comment from bharathwajv has anything to do with this particular issue.
@andrewsheridan HydratedStorageDirectory.web this should work!
This may have been fixed in the latest version of hive_ce... maybe? Probably not but worth a try. I haven't got around to trying it out yet.
https://github.com/IO-Design-Team/hive_ce/pull/139 https://github.com/IO-Design-Team/hive_ce/pull/138 https://github.com/IO-Design-Team/hive_ce/pull/132
Those PRs should not have changed any functionality
@andrewsheridan do you find any workaround for this issue?
In the example app overriding String get storagePrefix fixes it.
If you override storagePrefix in the CounterBloc but not the BrightnessBloc, the CounterBloc will keep its state, but the BrightnessBloc won't. (note that the first time you override storagePrefix the bloc will be reset.)
This makes sense when you read the documentation comment:
Storage prefix which can be overridden to provide a custom storage namespace. Defaults to [runtimeType] but should be overridden in cases where stored data should be resilient to obfuscation or persist between debug/release builds.
In the example app overriding
String get storagePrefixfixes it.If you override storagePrefix in the CounterBloc but not the BrightnessBloc, the CounterBloc will keep its state, but the BrightnessBloc won't. (note that the first time you override storagePrefix the bloc will be reset.)
This makes sense when you read the documentation comment:
Storage prefix which can be overridden to provide a custom storage namespace. Defaults to [runtimeType] but should be overridden in cases where stored data should be resilient to obfuscation or persist between debug/release builds.
but this will affect the existing users right? their stored data will be removed
In the example app overriding
String get storagePrefixfixes it.If you override storagePrefix in the CounterBloc but not the BrightnessBloc, the CounterBloc will keep its state, but the BrightnessBloc won't. (note that the first time you override storagePrefix the bloc will be reset.)
Good to know, this at least gives some direction going forward and reveals why my SharedPreferencesStorage workaround didn't end up working.
@JakesMD likely already knows this following bit, but I'm going to write it out loud to make sure my understanding is correct.
Sounds like the core issue is that web builds are getting minified or obfuscated in a way that the runtimeType of Blocs & Cubits is getting changed. When an update gets pushed out, maybe your CounterBloc was previously minified to a and in the new build it's been minified to b, and now the default storagePrefix is different.
Unfortunately this likely means that in order to recover the old data, you'd need a way to figure out what the runtimeType of CounterBloc was minified to in the old version, then go update your storagePrefix for CounterBloc to be that minified type.
If I've made any mistakes here, let me know. If anyone has any ideas on how to track down those obfuscated runtimeTypes, if it's even possible, I'd love to hear it.
Trying to think through another potential avenue for recovering data that involves accessing the Box inside of HydratedStorage.
- Get access to that Box
- Use
_box.keysto get the list of all the keys that have been used. - Iterate through those keys, retrieving the values associated to those keys.
- If the value is a data structure match for your Bloc/Cubit, rehydrate that Bloc/Cubit with that value.
For example, if you have a UserCubit which has a User with a username and id, and you find a value which successfully creates a User via User.fromJson(value), then you can recover that data.
Big shortcoming is that this wouldn't really help if you have a bunch of different HydratedBlocs which are storing something basic like a String.
Just throwing out ideas because I'm desperate.
@andrewsheridan How important is recovering the old data to you? If you were to override storagePrefix to say 'user_cubit' and deploy it, your users would lose their state one last time and (hopefully) never again in future releases of your app.
And yes, if you really needed to recover the old data from before you updated storagePrefix, your best option would be to find out what the minified runtimetype is and set storagePrefix to that. But I wouldn't know how to do that without releasing a new version of the app anyhow.
@JakesMD it's critical. I used HydratedBloc to store pretty much everything. So if I were to push out an update, users would lose all their data and be outraged.
Didn't know about this web issue at the time, so when I discovered it I just stopped updating the site until it could be resolved. So I haven't updated one of my sites in 2-3 years.
Using HydratedBloc on web was meant to be temporary. The plan was always to shift that data over to something like Firebase, but I can't do that without pushing out a new update, which would cause the data to be lost. And now that the web version has this many users, they'd be outraged if I wiped their data, and would ruin my reputation.
I was planning on just putting up a new site that uses a different storage mechanism, provide a way to port data from the old site, and give users a year or so to migrate. But even then, I had no way to inform users of the old site that they needed to migrate.
At least now I know the root cause and have some options. I do think it's worth really calling attention to this fallback of HydratedBloc in the documentation, or finding a good long-term solution that doesn't require developers to lose data first to find out about this issue.
You can access the box and therefore the runtime-types using the browser itself:
Now you just have to work out which is which using the values.
As far I can tell, some of the data is stored as ascii so you just have to convert it. For example:
12: 98 (b)
13: 114 (r)
14: 105 (i)
15: 103 (g)
16: 104 (h)
17: 116 (t)
18: 110 (n)
19: 101 (e)
20: 115 (s)
21: 115 (s)