antfu-collective / vitesse-webext

⚡️ WebExtension Vite Starter Template

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.