Storage reactivity
ManUtopiK opened this issue · comments
Describe the bug
Open the option page and the popup, then try to change storageDemo
value in the option page, it should change in the popup in realtime. It's not easy to see because the popup close when you activate the option page. With firefox, you can toggle the Disable popup auto-hide
option to see it in action.
It's particularly relevant with the opposite ; changing storageDemo
in the popup.
Furthermore, add this in the background :
import { storageDemo } from '~/logic/storage'
watch(storageDemo, (val) => {
console.log('storageDemo', storageDemo)
}, { immediate: true })
We can see that it's also not reactive in the background.
I know this issue #55, but the background, option page and popup «live» in the same context, not the contentScripts. The storage should be reactive between background, option page and popup.
Edit: I add this code below to the reproduction branch.
You can try with vueuse useStorage
by inserting this code in those three parts of the extension :
import { useStorage } from '@vueuse/core'
const localstorageDemo = useStorage('localstorageDemo', 'localstorage')
watch(localstorageDemo, (val) => {
console.log(val)
}, { immediate: true })
And add an input in the popup to edit the value :
<input v-model="storageDemo" class="border border-gray-400 rounded px-2 py-1 mt-2">
The reactivity works through the local storage reactivity provided by useStorage
.
Reproduction
https://github.com/ManUtopiK/vitesse-webext/tree/storageReactivityIssue
System Info
System:
OS: Linux 5.15 Manjaro Linux
CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
Memory: 7.31 GB / 31.14 GB
Container: Yes
Shell: 5.9 - /bin/zsh
Binaries:
Node: 18.3.0 - ~/.nvm/versions/node/v18.3.0/bin/node
Yarn: 1.22.19 - /usr/bin/yarn
npm: 8.11.0 - ~/.nvm/versions/node/v18.3.0/bin/npm
Browsers:
Chromium: 107.0.5304.121
Firefox: 107.0
Used Package Manager
pnpm
Validations
- Follow our Code of Conduct
- Read the Contributing Guide.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead.
- The provided reproduction is a minimal reproducible of the bug.
In fact, instead of launching three different parts of the extension, it could be the same vue instance in the three parts : background, popup and options page. And handle each part with vue-router
and the createWebHashHistory
.
It will simplify rollup building and vue plugins settings.
I use vue-i18n
and I have to pass the config for the three parts.
Maybe it's another issue...
Seems related to this: https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorageAsync/index.ts#L101
window
(or defaultWindow) needs to be replaced by browser
and the content of the callback must be a little tweaked to deal with the different format of the event
My current solution:
useWebExtensionStorage.ts
import { StorageSerializers } from '@vueuse/core'
import { toValue, watchWithFilter, tryOnScopeDispose } from '@vueuse/shared'
import { ref, shallowRef } from 'vue-demi'
import { storage } from 'webextension-polyfill'
import type {
UseStorageAsyncOptions,
StorageLikeAsync,
} from '@vueuse/core'
import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared'
import type { Ref } from 'vue-demi'
import type { Storage } from 'webextension-polyfill';
export type WebExtensionStorageOptions<T> = UseStorageAsyncOptions<T>
// https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorage/guess.ts
export function guessSerializerType<T extends(string | number | boolean | object | null)>(rawInit: T) {
return rawInit == null
? 'any'
: rawInit instanceof Set
? 'set'
: rawInit instanceof Map
? 'map'
: rawInit instanceof Date
? 'date'
: typeof rawInit === 'boolean'
? 'boolean'
: typeof rawInit === 'string'
? 'string'
: typeof rawInit === 'object'
? 'object'
: Number.isNaN(rawInit)
? 'any'
: 'number'
}
const storageInterface: StorageLikeAsync = {
removeItem(key: string) {
return storage.local.remove(key)
},
setItem(key: string, value: string) {
return storage.local.set({ [key]: value })
},
async getItem(key: string) {
const storedData = await storage.local.get(key)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return storedData[key]
},
}
/**
* https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorageAsync/index.ts
*
* @param key
* @param initialValue
* @param options
*/
export function useWebExtensionStorage<T extends(string | number | boolean | object | null)>(
key: string,
initialValue: MaybeRefOrGetter<T>,
options: WebExtensionStorageOptions<T> = {},
): RemovableRef<T> {
const {
flush = 'pre',
deep = true,
listenToStorageChanges = true,
writeDefaults = true,
mergeDefaults = false,
shallow,
eventFilter,
onError = (e) => {
console.error(e)
},
} = options
const rawInit: T = toValue(initialValue)
const type = guessSerializerType<T>(rawInit)
const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>
const serializer = options.serializer ?? StorageSerializers[type]
async function read(event?: { key: string, newValue: string | null }) {
if (event && event.key !== key) {
return
}
try {
const rawValue = event ? event.newValue : await storageInterface.getItem(key)
if (rawValue == null) {
data.value = rawInit
if (writeDefaults && rawInit !== null)
await storageInterface.setItem(key, await serializer.write(rawInit))
}
else if (mergeDefaults) {
const value = await serializer.read(rawValue) as T
if (typeof mergeDefaults === 'function')
data.value = mergeDefaults(value, rawInit)
else if (type === 'object' && !Array.isArray(value))
data.value = { ...(rawInit as Record<keyof unknown, unknown>), ...(value as Record<keyof unknown, unknown>) } as T
else data.value = value
}
else {
data.value = await serializer.read(rawValue) as T
}
}
catch (error) {
onError(error)
}
}
void read()
if (listenToStorageChanges) {
const listener = async (changes: Record<string, Storage.StorageChange>) => {
for (const [key, change] of Object.entries(changes)) {
await read({
key,
newValue: change.newValue as string | null,
})
}
}
storage.onChanged.addListener(listener)
tryOnScopeDispose(() => {
storage.onChanged.removeListener(listener);
})
}
watchWithFilter(
data,
async () => {
try {
await (data.value == null ? storageInterface.removeItem(key) : storageInterface.setItem(key, await serializer.write(data.value)));
}
catch (error) {
onError(error)
}
},
{
flush,
deep,
eventFilter,
},
)
return data as RemovableRef<T>
}
I just copied useStorageAsync and some internal stuff from vueuse and hardcoded the webextension storage interface. Should behave like any other useStorage from vueuse, but didnt test it extensively. Any suggestions are welcome.
Will create a PR later this week.